From 50e0597c84a4b7fd2c9fbf7cbf5464caea326c1e Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Fri, 5 Sep 2025 10:08:11 -0400 Subject: [PATCH] Pre-convo UI (#3376) --- skyvern-frontend/src/components/Orgwalled.tsx | 12 +- .../src/components/icons/BugIcon.tsx | 25 +++ .../src/components/icons/DebugIcon.tsx | 29 ++++ .../src/routes/tasks/create/PromptBox.tsx | 164 ++++-------------- .../src/routes/workflows/editor/Workspace.tsx | 2 +- .../editor/nodes/Taskv2Node/Taskv2Node.tsx | 2 + .../editor/nodes/components/NodeHeader.tsx | 29 +++- .../workflows/hooks/useCacheKeyValuesQuery.ts | 2 +- .../src/store/useAutoplayStore.ts | 24 +++ 9 files changed, 156 insertions(+), 133 deletions(-) create mode 100644 skyvern-frontend/src/components/icons/BugIcon.tsx create mode 100644 skyvern-frontend/src/components/icons/DebugIcon.tsx create mode 100644 skyvern-frontend/src/store/useAutoplayStore.ts diff --git a/skyvern-frontend/src/components/Orgwalled.tsx b/skyvern-frontend/src/components/Orgwalled.tsx index 53249245..37251591 100644 --- a/skyvern-frontend/src/components/Orgwalled.tsx +++ b/skyvern-frontend/src/components/Orgwalled.tsx @@ -11,9 +11,11 @@ import { cn } from "@/util/utils"; function OrgWalled({ children, className, + hideTooltipContent, }: { children: React.ReactNode; className?: string; + hideTooltipContent?: boolean; }) { const isSkyvernUser = useIsSkyvernUser(); @@ -35,9 +37,13 @@ function OrgWalled({ {children} - -

This feature is only available to Skyvern organization members

-
+ {!hideTooltipContent && ( + +

+ This feature is only available to Skyvern organization members +

+
+ )} ); diff --git a/skyvern-frontend/src/components/icons/BugIcon.tsx b/skyvern-frontend/src/components/icons/BugIcon.tsx new file mode 100644 index 00000000..f6f12846 --- /dev/null +++ b/skyvern-frontend/src/components/icons/BugIcon.tsx @@ -0,0 +1,25 @@ +type Props = { + className?: string; +}; + +function BugIcon({ className }: Props) { + return ( + + + + ); +} + +export { BugIcon }; diff --git a/skyvern-frontend/src/components/icons/DebugIcon.tsx b/skyvern-frontend/src/components/icons/DebugIcon.tsx new file mode 100644 index 00000000..36fe6741 --- /dev/null +++ b/skyvern-frontend/src/components/icons/DebugIcon.tsx @@ -0,0 +1,29 @@ +import { PaperPlaneIcon } from "@radix-ui/react-icons"; + +import { cn } from "@/util/utils"; +import { BugIcon } from "./BugIcon"; +import { Tip } from "../Tip"; + +type Props = { + className?: string; + // -- + onClick?: () => void; +}; + +function DebugIcon({ className, onClick }: Props) { + return ( + +
+ +
+ +
+
+
+ ); +} + +export { DebugIcon }; diff --git a/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx b/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx index 1a91c249..5b2aafba 100644 --- a/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx +++ b/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx @@ -1,10 +1,5 @@ import { getClient } from "@/api/AxiosClient"; -import { - Createv2TaskRequest, - TaskV2, - ProxyLocation, - TaskGenerationApiResponse, -} from "@/api/types"; +import { Createv2TaskRequest, ProxyLocation } from "@/api/types"; import img from "@/assets/promptBoxBg.png"; import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { CartIcon } from "@/components/icons/CartIcon"; @@ -26,6 +21,7 @@ import { import { Switch } from "@/components/ui/switch"; import { toast } from "@/components/ui/use-toast"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { FileTextIcon, @@ -38,7 +34,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { stringify as convertToYAML } from "yaml"; import { generatePhoneNumber, generateUniqueEmail, @@ -48,38 +43,7 @@ import { MAX_SCREENSHOT_SCROLLS_DEFAULT, MAX_STEPS_DEFAULT, } from "@/routes/workflows/editor/nodes/Taskv2Node/types"; - -function createTemplateTaskFromTaskGenerationParameters( - values: TaskGenerationApiResponse, -) { - return { - title: values.suggested_title ?? "Untitled Task", - description: "", - is_saved_task: true, - webhook_callback_url: null, - proxy_location: "RESIDENTIAL", - workflow_definition: { - parameters: [ - { - parameter_type: "workflow", - workflow_parameter_type: "json", - key: "navigation_payload", - default_value: JSON.stringify(values.navigation_payload), - }, - ], - blocks: [ - { - block_type: "task", - label: values.suggested_title ?? "Untitled Task", - url: values.url, - navigation_goal: values.navigation_goal, - data_extraction_goal: values.data_extraction_goal, - data_schema: values.extracted_information_schema, - }, - ], - }, - }; -} +import { useAutoplayStore } from "@/store/useAutoplayStore"; const exampleCases = [ { @@ -166,21 +130,22 @@ function PromptBox() { const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [dataSchema, setDataSchema] = useState(null); const [extraHttpHeaders, setExtraHttpHeaders] = useState(null); + const { setAutoplay } = useAutoplayStore(); - const startObserverCruiseMutation = useMutation({ + const generateWorkflowMutation = useMutation({ mutationFn: async (prompt: string) => { - const client = await getClient(credentialGetter, "v2"); - return client.post( - "/tasks", + const client = await getClient(credentialGetter, "sans-api-v1"); + + const result = await client.post< + Createv2TaskRequest, + { data: WorkflowApiResponse } + >( + "/workflows/create-from-prompt", { user_prompt: prompt, webhook_callback_url: webhookCallbackUrl, proxy_location: proxyLocation, - browser_session_id: browserSessionId, - browser_address: cdpAddress, totp_identifier: totpIdentifier, - generate_script: generateScript, - publish_workflow: publishWorkflow || generateScript, max_screenshot_scrolls: maxScreenshotScrolls, extracted_information_schema: dataSchema ? (() => { @@ -207,76 +172,32 @@ function PromptBox() { }, }, ); + + return result; }, - onSuccess: (response) => { + onSuccess: ({ data: workflow }) => { toast({ variant: "success", - title: "Workflow Run Created", - description: `Workflow run created successfully.`, - }); - queryClient.invalidateQueries({ - queryKey: ["workflowRuns"], + title: "Workflow Created", + description: `Workflow created successfully.`, }); + queryClient.invalidateQueries({ queryKey: ["workflows"], }); - queryClient.invalidateQueries({ - queryKey: ["runs"], - }); - navigate( - `/workflows/${response.data.workflow_permanent_id}/${response.data.workflow_run_id}`, - ); - }, - onError: (error: AxiosError) => { - toast({ - variant: "destructive", - title: "Error creating workflow run from prompt", - description: error.message, - }); - }, - }); - const getTaskFromPromptMutation = useMutation({ - mutationFn: async (prompt: string) => { - const client = await getClient(credentialGetter); - return client - .post< - { prompt: string }, - { data: TaskGenerationApiResponse } - >("/generate/task", { prompt }) - .then((response) => response.data); - }, - onError: (error: AxiosError) => { - const detail = (error.response?.data as { detail?: string })?.detail; - toast({ - variant: "destructive", - title: "Error creating task from prompt", - description: detail ? detail : error.message, - }); - }, - }); + const firstBlock = workflow.workflow_definition.blocks[0]; - const saveTaskMutation = useMutation({ - mutationFn: async (params: TaskGenerationApiResponse) => { - const client = await getClient(credentialGetter); - const templateTask = - createTemplateTaskFromTaskGenerationParameters(params); - const yaml = convertToYAML(templateTask); - return client.post("/workflows", yaml, { - headers: { - "Content-Type": "text/plain", - }, - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ["savedTasks"], - }); + if (firstBlock) { + setAutoplay(workflow.workflow_permanent_id, firstBlock.label); + } + + navigate(`/workflows/${workflow.workflow_permanent_id}/debug`); }, onError: (error: AxiosError) => { toast({ variant: "destructive", - title: "Error saving task", + title: "Error creating workflow from prompt", description: error.message, }); }, @@ -343,30 +264,21 @@ function PromptBox() { />
- {startObserverCruiseMutation.isPending || - getTaskFromPromptMutation.isPending || - saveTaskMutation.isPending ? ( + {generateWorkflowMutation.isPending ? ( ) : ( - { - if (selectValue === "v2") { - startObserverCruiseMutation.mutate(prompt); - return; - } - const taskGenerationResponse = - await getTaskFromPromptMutation.mutateAsync(prompt); - await saveTaskMutation.mutateAsync( - taskGenerationResponse, - ); - navigate("/tasks/create/from-prompt", { - state: { - data: taskGenerationResponse, - }, - }); - }} - /> +
+ {generateWorkflowMutation.isPending ? ( + + ) : ( + { + generateWorkflowMutation.mutate(prompt); + }} + /> + )} +
)}
diff --git a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx index 3992cefe..ae831249 100644 --- a/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/Workspace.tsx @@ -98,7 +98,7 @@ function Workspace({ const { blockLabel, workflowPermanentId } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); const cacheKeyValueParam = searchParams.get("cache-key-value"); - const [timelineMode, setTimelineMode] = useState("narrow"); + const [timelineMode, setTimelineMode] = useState("wide"); const [cacheKeyValueFilter, setCacheKeyValueFilter] = useState( null, ); 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 9bc3759f..963d6654 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx @@ -17,6 +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 { useParams } from "react-router-dom"; import { statusIsRunningOrQueued } from "@/routes/tasks/types"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; @@ -195,6 +196,7 @@ function Taskv2Node({ id, data, type }: NodeProps) { + ); diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx index 835aba2d..ace12389 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx @@ -11,6 +11,7 @@ import { toast } from "@/components/ui/use-toast"; import { useLogging } from "@/hooks/useLogging"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useOnChange } from "@/hooks/useOnChange"; +import { useAutoplayStore } from "@/store/useAutoplayStore"; import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; @@ -186,6 +187,28 @@ function NodeHeader({ const [workflowRunStatus, setWorkflowRunStatus] = useState( workflowRun?.status, ); + const { getAutoplay, setAutoplay } = useAutoplayStore(); + + useEffect(() => { + if (!debugSession) { + return; + } + + const details = getAutoplay(); + + if ( + workflowPermanentId === details.wpid && + blockLabel === details.blockLabel + ) { + setAutoplay(null, null); + setTimeout(() => { + runBlock.mutateAsync(); + }, 100); + } + + // on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debugSession]); useEffect(() => { setWorkflowRunStatus(workflowRun?.status); @@ -496,8 +519,10 @@ function NodeHeader({ ) : ( { handleOnPlay(); diff --git a/skyvern-frontend/src/routes/workflows/hooks/useCacheKeyValuesQuery.ts b/skyvern-frontend/src/routes/workflows/hooks/useCacheKeyValuesQuery.ts index c9b28139..bd3f1313 100644 --- a/skyvern-frontend/src/routes/workflows/hooks/useCacheKeyValuesQuery.ts +++ b/skyvern-frontend/src/routes/workflows/hooks/useCacheKeyValuesQuery.ts @@ -56,7 +56,7 @@ function useCacheKeyValuesQuery({ return result; }, - enabled: !!workflowPermanentId, + enabled: !!workflowPermanentId && !!cacheKey && cacheKey.length > 0, placeholderData: keepPreviousData, staleTime: 5 * 60 * 1000, }); diff --git a/skyvern-frontend/src/store/useAutoplayStore.ts b/skyvern-frontend/src/store/useAutoplayStore.ts new file mode 100644 index 00000000..d8102085 --- /dev/null +++ b/skyvern-frontend/src/store/useAutoplayStore.ts @@ -0,0 +1,24 @@ +import { create } from "zustand"; + +type AutoplayStore = { + wpid: string | null; + blockLabel: string | null; + setAutoplay: (wpid: string | null, blockLabel: string | null) => void; + clearAutoplay: () => void; + getAutoplay: () => { wpid: string | null; blockLabel: string | null }; +}; + +export const useAutoplayStore = create((set, get) => ({ + wpid: null, + blockLabel: null, + setAutoplay: (wpid: string | null, blockLabel: string | null) => { + set({ wpid, blockLabel }); + }, + clearAutoplay: () => { + set({ wpid: null, blockLabel: null }); + }, + getAutoplay: () => { + const { wpid, blockLabel } = get(); + return { wpid, blockLabel }; + }, +}));