MVP Debugger UI (#2888)
This commit is contained in:
50
skyvern-frontend/package-lock.json
generated
50
skyvern-frontend/package-lock.json
generated
@@ -48,8 +48,10 @@
|
||||
"nanoid": "^5.0.7",
|
||||
"open": "^10.1.0",
|
||||
"posthog-js": "^1.138.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hook-form": "^7.51.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
@@ -4692,9 +4694,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
|
||||
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -7372,6 +7375,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -7453,6 +7467,16 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/re-resizable": {
|
||||
"version": "6.11.2",
|
||||
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz",
|
||||
"integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
@@ -7476,6 +7500,20 @@
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-draggable": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz",
|
||||
"integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-github-btn": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz",
|
||||
@@ -7502,6 +7540,12 @@
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
||||
|
||||
@@ -56,8 +56,10 @@
|
||||
"nanoid": "^5.0.7",
|
||||
"open": "^10.1.0",
|
||||
"posthog-js": "^1.138.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hook-form": "^7.51.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Status } from "@/api/types";
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { HandIcon, PlayIcon } from "@radix-ui/react-icons";
|
||||
import { HandIcon, StopIcon } from "@radix-ui/react-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { statusIsNotFinalized } from "@/routes/tasks/types";
|
||||
@@ -317,13 +317,15 @@ function BrowserStream({
|
||||
<div className="overlay-container">
|
||||
<div className="overlay">
|
||||
<Button
|
||||
// className="take-control"
|
||||
className={cn("take-control", { hide: userIsControlling })}
|
||||
className={cn(
|
||||
"take-control absolute bottom-[-1rem] left-[1rem]",
|
||||
{ hide: userIsControlling },
|
||||
)}
|
||||
type="button"
|
||||
onClick={() => setUserIsControlling(true)}
|
||||
>
|
||||
<HandIcon className="mr-2 h-4 w-4" />
|
||||
take control
|
||||
interact
|
||||
</Button>
|
||||
<div className="absolute bottom-[-1rem] right-[1rem]">
|
||||
<Button
|
||||
@@ -333,8 +335,8 @@ function BrowserStream({
|
||||
type="button"
|
||||
onClick={() => setUserIsControlling(false)}
|
||||
>
|
||||
<PlayIcon className="mr-2 h-4 w-4" />
|
||||
run agent
|
||||
<StopIcon className="mr-2 h-4 w-4" />
|
||||
stop interacting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
54
skyvern-frontend/src/components/Timer.tsx
Normal file
54
skyvern-frontend/src/components/Timer.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface HMS {
|
||||
hour: number;
|
||||
minute: number;
|
||||
second: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
startAt?: HMS;
|
||||
}
|
||||
|
||||
function Timer({ startAt }: Props) {
|
||||
const [time, setTime] = useState<HMS>({
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const start = performance.now();
|
||||
|
||||
const loop = () => {
|
||||
const elapsed = performance.now() - start;
|
||||
let seconds = Math.floor(elapsed / 1000);
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
seconds = seconds % 60;
|
||||
minutes = minutes % 60;
|
||||
hours = hours % 24;
|
||||
setTime(() => ({
|
||||
hour: hours + (startAt?.hour ?? 0),
|
||||
minute: minutes + (startAt?.minute ?? 0),
|
||||
second: seconds + (startAt?.second ?? 0),
|
||||
}));
|
||||
|
||||
rAF = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
let rAF = requestAnimationFrame(loop);
|
||||
|
||||
return () => cancelAnimationFrame(rAF);
|
||||
}, [startAt]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{String(time.hour).padStart(2, "0")}:
|
||||
{String(time.minute).padStart(2, "0")}:
|
||||
{String(time.second).padStart(2, "0")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Timer };
|
||||
17
skyvern-frontend/src/hooks/useOnChange.ts
Normal file
17
skyvern-frontend/src/hooks/useOnChange.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
function useOnChange<T>(
|
||||
value: T,
|
||||
callback: (newValue: T, prevValue: T | undefined) => void,
|
||||
) {
|
||||
const prevValue = useRef<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevValue.current !== undefined) {
|
||||
callback(value, prevValue.current);
|
||||
}
|
||||
prevValue.current = value;
|
||||
}, [value, callback]);
|
||||
}
|
||||
|
||||
export { useOnChange };
|
||||
@@ -22,11 +22,16 @@ 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 { DebugStoreProvider } from "@/store/DebugStoreContext";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <RootLayout />,
|
||||
element: (
|
||||
<DebugStoreProvider>
|
||||
<RootLayout />
|
||||
</DebugStoreProvider>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -98,6 +103,14 @@ const router = createBrowserRouter([
|
||||
index: true,
|
||||
element: <Navigate to="runs" />,
|
||||
},
|
||||
{
|
||||
path: "debug",
|
||||
element: <WorkflowEditor />,
|
||||
},
|
||||
{
|
||||
path: ":workflowRunId/:blockLabel/debug",
|
||||
element: <WorkflowEditor />,
|
||||
},
|
||||
{
|
||||
path: "edit",
|
||||
element: <WorkflowEditor />,
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { DiscordLogoIcon } from "@radix-ui/react-icons";
|
||||
import GitHubButton from "react-github-btn";
|
||||
import { Link, useMatch } from "react-router-dom";
|
||||
import { Link, useMatch, useSearchParams } from "react-router-dom";
|
||||
import { NavigationHamburgerMenu } from "./NavigationHamburgerMenu";
|
||||
|
||||
function Header() {
|
||||
const match = useMatch("/workflows/:workflowPermanentId/edit");
|
||||
const [searchParams] = useSearchParams();
|
||||
const embed = searchParams.get("embed");
|
||||
const match =
|
||||
useMatch("/workflows/:workflowPermanentId/edit") ||
|
||||
location.pathname.includes("debug") ||
|
||||
embed === "true";
|
||||
|
||||
if (match) {
|
||||
return null;
|
||||
|
||||
@@ -4,18 +4,24 @@ import { cn } from "@/util/utils";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Header } from "./Header";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
|
||||
function RootLayout() {
|
||||
const collapsed = useSidebarStore((state) => state.collapsed);
|
||||
const embed = new URLSearchParams(window.location.search).get("embed");
|
||||
const isEmbedded = embed === "true";
|
||||
const debugStore = useDebugStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isEmbedded && <Sidebar />}
|
||||
<div className="h-full w-full">
|
||||
<Sidebar />
|
||||
<Header />
|
||||
<main
|
||||
className={cn("lg:pb-4 lg:pl-64", {
|
||||
"lg:pl-28": collapsed,
|
||||
"lg:pl-4": isEmbedded,
|
||||
"lg:pb-0": debugStore.isDebugMode,
|
||||
})}
|
||||
>
|
||||
<Outlet />
|
||||
|
||||
@@ -30,6 +30,8 @@ import { WorkflowParameterInput } from "./WorkflowParameterInput";
|
||||
import { AxiosError } from "axios";
|
||||
import { getLabelForWorkflowParameterType } from "./editor/workflowEditorUtils";
|
||||
import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "./editor/nodes/Taskv2Node/types";
|
||||
import { lsKeys } from "@/util/env";
|
||||
|
||||
type Props = {
|
||||
workflowParameters: Array<WorkflowParameter>;
|
||||
initialValues: Record<string, unknown>;
|
||||
@@ -145,7 +147,7 @@ function RunWorkflowForm({
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const browserSessionIdDefault = useLocalStorageFormDefault(
|
||||
"skyvern.browserSessionId",
|
||||
lsKeys.browserSessionId,
|
||||
(initialValues.browserSessionId as string | undefined) ?? null,
|
||||
);
|
||||
const form = useForm<RunWorkflowFormType>({
|
||||
@@ -162,11 +164,7 @@ function RunWorkflowForm({
|
||||
});
|
||||
const apiCredential = useApiCredential();
|
||||
|
||||
useSyncFormFieldToStorage(
|
||||
form,
|
||||
"browserSessionId",
|
||||
"skyvern.browserSessionId",
|
||||
);
|
||||
useSyncFormFieldToStorage(form, "browserSessionId", lsKeys.browserSessionId);
|
||||
|
||||
const runWorkflowMutation = useMutation({
|
||||
mutationFn: async (values: RunWorkflowFormType) => {
|
||||
|
||||
@@ -41,6 +41,8 @@ import { type ApiCommandOptions } from "@/util/apiCommands";
|
||||
|
||||
function WorkflowRun() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const embed = searchParams.get("embed");
|
||||
const isEmbedded = embed === "true";
|
||||
const active = searchParams.get("active");
|
||||
const { workflowRunId, workflowPermanentId } = useParams();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
@@ -169,92 +171,94 @@ function WorkflowRun() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header className="flex justify-between">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-5">
|
||||
{title}
|
||||
{workflowRunIsLoading ? (
|
||||
<Skeleton className="h-8 w-28" />
|
||||
) : workflowRun ? (
|
||||
<StatusBadge status={workflowRun?.status} />
|
||||
) : null}
|
||||
{!isEmbedded && (
|
||||
<header className="flex justify-between">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-5">
|
||||
{title}
|
||||
{workflowRunIsLoading ? (
|
||||
<Skeleton className="h-8 w-28" />
|
||||
) : workflowRun ? (
|
||||
<StatusBadge status={workflowRun?.status} />
|
||||
) : null}
|
||||
</div>
|
||||
<h2 className="text-2xl text-slate-400">{workflowRunId}</h2>
|
||||
</div>
|
||||
<h2 className="text-2xl text-slate-400">{workflowRunId}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<CopyApiCommandDropdown
|
||||
getOptions={() =>
|
||||
({
|
||||
method: "POST",
|
||||
url: `${apiBaseUrl}/workflows/${workflowPermanentId}/run`,
|
||||
body: {
|
||||
data: workflowRun?.parameters,
|
||||
proxy_location: "RESIDENTIAL",
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiCredential ?? "<your-api-key>",
|
||||
},
|
||||
}) satisfies ApiCommandOptions
|
||||
}
|
||||
/>
|
||||
<Button asChild variant="secondary">
|
||||
<Link to={`/workflows/${workflowPermanentId}/edit`}>
|
||||
<Pencil2Icon className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</Button>
|
||||
{workflowRunIsRunningOrQueued && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">Cancel</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to cancel this workflow run?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">Back</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
cancelWorkflowMutation.mutate();
|
||||
}}
|
||||
disabled={cancelWorkflowMutation.isPending}
|
||||
>
|
||||
{cancelWorkflowMutation.isPending && (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Cancel Workflow Run
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{workflowRunIsFinalized && !isTaskv2Run && (
|
||||
<Button asChild>
|
||||
<Link
|
||||
to={`/workflows/${workflowPermanentId}/run`}
|
||||
state={{
|
||||
data: parameters,
|
||||
proxyLocation,
|
||||
webhookCallbackUrl: workflowRun?.webhook_callback_url ?? "",
|
||||
maxScreenshotScrolls,
|
||||
}}
|
||||
>
|
||||
<PlayIcon className="mr-2 h-4 w-4" />
|
||||
Rerun
|
||||
<div className="flex gap-2">
|
||||
<CopyApiCommandDropdown
|
||||
getOptions={() =>
|
||||
({
|
||||
method: "POST",
|
||||
url: `${apiBaseUrl}/workflows/${workflowPermanentId}/run`,
|
||||
body: {
|
||||
data: workflowRun?.parameters,
|
||||
proxy_location: "RESIDENTIAL",
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiCredential ?? "<your-api-key>",
|
||||
},
|
||||
}) satisfies ApiCommandOptions
|
||||
}
|
||||
/>
|
||||
<Button asChild variant="secondary">
|
||||
<Link to={`/workflows/${workflowPermanentId}/edit`}>
|
||||
<Pencil2Icon className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
{workflowRunIsRunningOrQueued && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">Cancel</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to cancel this workflow run?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">Back</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
cancelWorkflowMutation.mutate();
|
||||
}}
|
||||
disabled={cancelWorkflowMutation.isPending}
|
||||
>
|
||||
{cancelWorkflowMutation.isPending && (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Cancel Workflow Run
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{workflowRunIsFinalized && !isTaskv2Run && (
|
||||
<Button asChild>
|
||||
<Link
|
||||
to={`/workflows/${workflowPermanentId}/run`}
|
||||
state={{
|
||||
data: parameters,
|
||||
proxyLocation,
|
||||
webhookCallbackUrl: workflowRun?.webhook_callback_url ?? "",
|
||||
maxScreenshotScrolls,
|
||||
}}
|
||||
>
|
||||
<PlayIcon className="mr-2 h-4 w-4" />
|
||||
Rerun
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
{showOutputSection && (
|
||||
<div
|
||||
className={cn("grid gap-4 rounded-lg bg-slate-elevation1 p-4", {
|
||||
@@ -307,26 +311,28 @@ function WorkflowRun() {
|
||||
</div>
|
||||
)}
|
||||
{workflowFailureReason}
|
||||
<SwitchBarNavigation
|
||||
options={[
|
||||
{
|
||||
label: "Overview",
|
||||
to: "overview",
|
||||
},
|
||||
{
|
||||
label: "Output",
|
||||
to: "output",
|
||||
},
|
||||
{
|
||||
label: "Parameters",
|
||||
to: "parameters",
|
||||
},
|
||||
{
|
||||
label: "Recording",
|
||||
to: "recording",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{!isEmbedded && (
|
||||
<SwitchBarNavigation
|
||||
options={[
|
||||
{
|
||||
label: "Overview",
|
||||
to: "overview",
|
||||
},
|
||||
{
|
||||
label: "Output",
|
||||
to: "output",
|
||||
},
|
||||
{
|
||||
label: "Parameters",
|
||||
to: "parameters",
|
||||
},
|
||||
{
|
||||
label: "Recording",
|
||||
to: "recording",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<div className="flex h-[42rem] gap-6">
|
||||
<div className="w-2/3">
|
||||
<Outlet />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RunWorkflowForm } from "./RunWorkflowForm";
|
||||
import { WorkflowApiResponse } from "./types/workflowTypes";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import { getInitialValues } from "./utils";
|
||||
|
||||
function WorkflowRunParameters() {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
@@ -30,6 +31,7 @@ function WorkflowRunParameters() {
|
||||
const proxyLocation = location.state
|
||||
? (location.state.proxyLocation as ProxyLocation)
|
||||
: null;
|
||||
|
||||
const maxScreenshotScrolls = location.state?.maxScreenshotScrolls ?? null;
|
||||
|
||||
const webhookCallbackUrl = location.state
|
||||
@@ -40,43 +42,7 @@ function WorkflowRunParameters() {
|
||||
? (location.state.extraHttpHeaders as Record<string, string>)
|
||||
: null;
|
||||
|
||||
const initialValues = location.state?.data
|
||||
? location.state.data
|
||||
: workflowParameters?.reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.workflow_parameter_type === "json") {
|
||||
if (typeof curr.default_value === "string") {
|
||||
acc[curr.key] = curr.default_value;
|
||||
return acc;
|
||||
}
|
||||
if (curr.default_value) {
|
||||
acc[curr.key] = JSON.stringify(curr.default_value, null, 2);
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
if (
|
||||
curr.default_value &&
|
||||
curr.workflow_parameter_type === "boolean"
|
||||
) {
|
||||
acc[curr.key] = Boolean(curr.default_value);
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
curr.default_value === null &&
|
||||
curr.workflow_parameter_type === "string"
|
||||
) {
|
||||
acc[curr.key] = "";
|
||||
return acc;
|
||||
}
|
||||
if (curr.default_value) {
|
||||
acc[curr.key] = curr.default_value;
|
||||
return acc;
|
||||
}
|
||||
acc[curr.key] = null;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
);
|
||||
const initialValues = getInitialValues(location, workflowParameters ?? []);
|
||||
|
||||
const header = (
|
||||
<header className="space-y-5">
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { cn } from "@/util/utils";
|
||||
import { Outlet, useMatch } from "react-router-dom";
|
||||
import { Outlet, useMatch, useSearchParams } from "react-router-dom";
|
||||
|
||||
function WorkflowsPageLayout() {
|
||||
const match = useMatch("/workflows/:workflowPermanentId/edit");
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const embed = searchParams.get("embed");
|
||||
const match =
|
||||
useMatch("/workflows/:workflowPermanentId/edit") ||
|
||||
location.pathname.includes("debug") ||
|
||||
embed === "true";
|
||||
return (
|
||||
<main
|
||||
className={cn({
|
||||
|
||||
@@ -10,8 +10,10 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useOnChange } from "@/hooks/useOnChange";
|
||||
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
||||
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
@@ -22,10 +24,12 @@ import {
|
||||
Controls,
|
||||
Edge,
|
||||
Panel,
|
||||
PanOnScrollMode,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesInitialized,
|
||||
useNodesState,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { AxiosError } from "axios";
|
||||
@@ -35,6 +39,7 @@ import { useBlocker, useParams } from "react-router-dom";
|
||||
import { stringify as convertToYAML } from "yaml";
|
||||
import {
|
||||
AWSSecretParameter,
|
||||
debuggableWorkflowBlockTypes,
|
||||
WorkflowApiResponse,
|
||||
WorkflowEditorParameterTypes,
|
||||
WorkflowParameterTypes,
|
||||
@@ -91,7 +96,7 @@ import {
|
||||
nodeAdderNode,
|
||||
startNode,
|
||||
} from "./workflowEditorUtils";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
import { useAutoPan } from "./useAutoPan";
|
||||
|
||||
function convertToParametersYAML(
|
||||
@@ -254,6 +259,8 @@ function FlowRenderer({
|
||||
initialParameters,
|
||||
workflow,
|
||||
}: Props) {
|
||||
const reactFlowInstance = useReactFlow();
|
||||
const debugStore = useDebugStore();
|
||||
const { workflowPermanentId } = useParams();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -264,6 +271,7 @@ function FlowRenderer({
|
||||
const [parameters, setParameters] =
|
||||
useState<ParametersState>(initialParameters);
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
const [debuggableBlockCount, setDebuggableBlockCount] = useState(0);
|
||||
const nodesInitialized = useNodesInitialized();
|
||||
const { hasChanges, setHasChanges } = useWorkflowHasChangesStore();
|
||||
useShouldNotifyWhenClosingTab(hasChanges);
|
||||
@@ -379,6 +387,14 @@ function FlowRenderer({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodesInitialized]);
|
||||
|
||||
useEffect(() => {
|
||||
const blocks = getWorkflowBlocks(nodes, edges);
|
||||
const debuggable = blocks.filter((block) =>
|
||||
debuggableWorkflowBlockTypes.has(block.block_type),
|
||||
);
|
||||
setDebuggableBlockCount(debuggable.length);
|
||||
}, [nodes, edges]);
|
||||
|
||||
async function handleSave() {
|
||||
const blocks = getWorkflowBlocks(nodes, edges);
|
||||
const settings = getWorkflowSettings(nodes);
|
||||
@@ -590,6 +606,47 @@ function FlowRenderer({
|
||||
|
||||
useAutoPan(editorElementRef, nodes);
|
||||
|
||||
const zoomLock = 1 as const;
|
||||
const yLockMax = 140 as const;
|
||||
|
||||
/**
|
||||
* TODO(jdo): hack
|
||||
*/
|
||||
const getXLock = () => {
|
||||
const hasForLoopNode = nodes.some((node) => node.type === "loop");
|
||||
return hasForLoopNode ? 24 : 104;
|
||||
};
|
||||
|
||||
useOnChange(debugStore.isDebugMode, (newValue) => {
|
||||
const xLock = getXLock();
|
||||
if (newValue) {
|
||||
const currentY = reactFlowInstance.getViewport().y;
|
||||
reactFlowInstance.setViewport({ x: xLock, y: currentY, zoom: zoomLock });
|
||||
}
|
||||
});
|
||||
|
||||
const constrainPan = (y: number) => {
|
||||
const yLockMin = nodes.reduce(
|
||||
(acc, node) => {
|
||||
const nodeBottom = node.position.y + (node.height ?? 0);
|
||||
if (nodeBottom > acc.value) {
|
||||
return { value: nodeBottom };
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ value: -Infinity },
|
||||
);
|
||||
|
||||
const yLockMinValue = yLockMin.value;
|
||||
const xLock = getXLock();
|
||||
const newY = Math.max(-yLockMinValue + yLockMax, Math.min(yLockMax, y));
|
||||
reactFlowInstance.setViewport({
|
||||
x: xLock,
|
||||
y: newY,
|
||||
zoom: zoomLock,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
@@ -677,16 +734,33 @@ function FlowRenderer({
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
colorMode="dark"
|
||||
fitView
|
||||
fitView={!debugStore.isDebugMode}
|
||||
fitViewOptions={{
|
||||
maxZoom: 1,
|
||||
}}
|
||||
deleteKeyCode={null}
|
||||
onMove={(_, viewport) => {
|
||||
const y = viewport.y;
|
||||
debugStore.isDebugMode && constrainPan(y);
|
||||
}}
|
||||
maxZoom={debugStore.isDebugMode ? 1 : 2}
|
||||
minZoom={debugStore.isDebugMode ? 1 : 0.5}
|
||||
panOnDrag={!debugStore.isDebugMode}
|
||||
panOnScroll={debugStore.isDebugMode}
|
||||
panOnScrollMode={
|
||||
debugStore.isDebugMode
|
||||
? PanOnScrollMode.Vertical
|
||||
: PanOnScrollMode.Free
|
||||
}
|
||||
zoomOnDoubleClick={!debugStore.isDebugMode}
|
||||
zoomOnPinch={!debugStore.isDebugMode}
|
||||
zoomOnScroll={!debugStore.isDebugMode}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
||||
<Controls position="bottom-left" />
|
||||
<Panel position="top-center" className="h-20">
|
||||
<Panel position="top-center" className={cn("h-20")}>
|
||||
<WorkflowHeader
|
||||
debuggableBlockCount={debuggableBlockCount}
|
||||
title={title}
|
||||
saving={saveWorkflowMutation.isPending}
|
||||
onTitleChange={(newTitle) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||
import { FlowRenderer } from "./FlowRenderer";
|
||||
import { getElements } from "./workflowEditorUtils";
|
||||
import { LogoMinimized } from "@/components/LogoMinimized";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import {
|
||||
isDisplayedInWorkflowEditor,
|
||||
WorkflowEditorParameterTypes,
|
||||
@@ -16,8 +17,11 @@ import {
|
||||
} from "../types/workflowTypes";
|
||||
import { ParametersState } from "./types";
|
||||
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
|
||||
import { WorkflowDebugOverviewWindow } from "./panels/WorkflowDebugOverviewWindow";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
function WorkflowEditor() {
|
||||
const debugStore = useDebugStore();
|
||||
const { workflowPermanentId } = useParams();
|
||||
const setCollapsed = useSidebarStore((state) => {
|
||||
return state.setCollapsed;
|
||||
@@ -72,110 +76,123 @@ function WorkflowEditor() {
|
||||
!isGlobalWorkflow,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
<ReactFlowProvider>
|
||||
<FlowRenderer
|
||||
initialTitle={workflow.title}
|
||||
initialNodes={elements.nodes}
|
||||
initialEdges={elements.edges}
|
||||
initialParameters={
|
||||
workflow.workflow_definition.parameters
|
||||
.filter((parameter) => isDisplayedInWorkflowEditor(parameter))
|
||||
.map((parameter) => {
|
||||
if (
|
||||
parameter.parameter_type === WorkflowParameterTypes.Workflow
|
||||
) {
|
||||
if (
|
||||
parameter.workflow_parameter_type ===
|
||||
WorkflowParameterValueType.CredentialId
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Credential,
|
||||
credentialId: parameter.default_value as string,
|
||||
description: parameter.description,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Workflow,
|
||||
dataType: parameter.workflow_parameter_type,
|
||||
defaultValue: parameter.default_value,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type === WorkflowParameterTypes.Context
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Context,
|
||||
sourceParameterKey: parameter.source.key,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type ===
|
||||
WorkflowParameterTypes.Bitwarden_Sensitive_Information
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Secret,
|
||||
collectionId: parameter.bitwarden_collection_id,
|
||||
identityKey: parameter.bitwarden_identity_key,
|
||||
identityFields: parameter.bitwarden_identity_fields,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type ===
|
||||
WorkflowParameterTypes.Bitwarden_Credit_Card_Data
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.CreditCardData,
|
||||
collectionId: parameter.bitwarden_collection_id,
|
||||
itemId: parameter.bitwarden_item_id,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type === WorkflowParameterTypes.Credential
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Credential,
|
||||
credentialId: parameter.credential_id,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type ===
|
||||
WorkflowParameterTypes.OnePassword
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.OnePassword,
|
||||
vaultId: parameter.vault_id,
|
||||
itemId: parameter.item_id,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type ===
|
||||
WorkflowParameterTypes.Bitwarden_Login_Credential
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Credential,
|
||||
collectionId: parameter.bitwarden_collection_id,
|
||||
itemId: parameter.bitwarden_item_id,
|
||||
urlParameterKey: parameter.url_parameter_key,
|
||||
description: parameter.description,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(Boolean) as ParametersState
|
||||
const getInitialParameters = () => {
|
||||
return workflow.workflow_definition.parameters
|
||||
.filter((parameter) => isDisplayedInWorkflowEditor(parameter))
|
||||
.map((parameter) => {
|
||||
if (parameter.parameter_type === WorkflowParameterTypes.Workflow) {
|
||||
if (
|
||||
parameter.workflow_parameter_type ===
|
||||
WorkflowParameterValueType.CredentialId
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Credential,
|
||||
credentialId: parameter.default_value as string,
|
||||
description: parameter.description,
|
||||
};
|
||||
}
|
||||
workflow={workflow}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Workflow,
|
||||
dataType: parameter.workflow_parameter_type,
|
||||
defaultValue: parameter.default_value,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type === WorkflowParameterTypes.Context
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Context,
|
||||
sourceParameterKey: parameter.source.key,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type ===
|
||||
WorkflowParameterTypes.Bitwarden_Sensitive_Information
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Secret,
|
||||
collectionId: parameter.bitwarden_collection_id,
|
||||
identityKey: parameter.bitwarden_identity_key,
|
||||
identityFields: parameter.bitwarden_identity_fields,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type ===
|
||||
WorkflowParameterTypes.Bitwarden_Credit_Card_Data
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.CreditCardData,
|
||||
collectionId: parameter.bitwarden_collection_id,
|
||||
itemId: parameter.bitwarden_item_id,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type === WorkflowParameterTypes.Credential
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Credential,
|
||||
credentialId: parameter.credential_id,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type === WorkflowParameterTypes.OnePassword
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.OnePassword,
|
||||
vaultId: parameter.vault_id,
|
||||
itemId: parameter.item_id,
|
||||
description: parameter.description,
|
||||
};
|
||||
} else if (
|
||||
parameter.parameter_type ===
|
||||
WorkflowParameterTypes.Bitwarden_Login_Credential
|
||||
) {
|
||||
return {
|
||||
key: parameter.key,
|
||||
parameterType: WorkflowEditorParameterTypes.Credential,
|
||||
collectionId: parameter.bitwarden_collection_id,
|
||||
itemId: parameter.bitwarden_item_id,
|
||||
urlParameterKey: parameter.url_parameter_key,
|
||||
description: parameter.description,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(Boolean) as ParametersState;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen w-full">
|
||||
<div
|
||||
className={cn("h-full w-full", {
|
||||
"w-[43.5rem] border-r border-slate-600": debugStore.isDebugMode,
|
||||
})}
|
||||
>
|
||||
<ReactFlowProvider>
|
||||
<FlowRenderer
|
||||
initialEdges={elements.edges}
|
||||
initialNodes={elements.nodes}
|
||||
initialParameters={getInitialParameters()}
|
||||
initialTitle={workflow.title}
|
||||
workflow={workflow}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
{debugStore.isDebugMode && (
|
||||
<div
|
||||
className="relative h-full w-full p-6"
|
||||
style={{ width: "calc(100% - 43.5rem)" }}
|
||||
>
|
||||
<WorkflowDebugOverviewWindow />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
CopyIcon,
|
||||
Crosshair1Icon,
|
||||
PlayIcon,
|
||||
ReloadIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
@@ -18,8 +19,11 @@ import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
|
||||
import { EditableNodeTitle } from "./nodes/components/EditableNodeTitle";
|
||||
import { useCreateWorkflowMutation } from "../hooks/useCreateWorkflowMutation";
|
||||
import { convert } from "./workflowEditorUtils";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
type Props = {
|
||||
debuggableBlockCount: number;
|
||||
title: string;
|
||||
parametersPanelOpen: boolean;
|
||||
onParametersClick: () => void;
|
||||
@@ -29,6 +33,7 @@ type Props = {
|
||||
};
|
||||
|
||||
function WorkflowHeader({
|
||||
debuggableBlockCount,
|
||||
title,
|
||||
parametersPanelOpen,
|
||||
onParametersClick,
|
||||
@@ -36,10 +41,13 @@ function WorkflowHeader({
|
||||
onTitleChange,
|
||||
saving,
|
||||
}: Props) {
|
||||
const { workflowPermanentId } = useParams();
|
||||
const { blockLabel: urlBlockLabel, workflowPermanentId } = useParams();
|
||||
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
|
||||
const navigate = useNavigate();
|
||||
const createWorkflowMutation = useCreateWorkflowMutation();
|
||||
const debugStore = useDebugStore();
|
||||
const anyBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel.length > 0;
|
||||
|
||||
if (!globalWorkflows) {
|
||||
return null; // this should be loaded already by some other components
|
||||
@@ -50,7 +58,11 @@ function WorkflowHeader({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full justify-between rounded-xl bg-slate-elevation2 px-6 py-5">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full justify-between rounded-xl bg-slate-elevation2 px-6 py-5",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full items-center">
|
||||
<EditableNodeTitle
|
||||
editable={true}
|
||||
@@ -85,6 +97,21 @@ function WorkflowHeader({
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
size="lg"
|
||||
variant={debugStore.isDebugMode ? "default" : "tertiary"}
|
||||
disabled={debuggableBlockCount === 0 || anyBlockIsPlaying}
|
||||
onClick={() => {
|
||||
if (debugStore.isDebugMode) {
|
||||
navigate(`/workflows/${workflowPermanentId}/edit`);
|
||||
} else {
|
||||
navigate(`/workflows/${workflowPermanentId}/debug`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Crosshair1Icon className="mr-2 h-6 w-6" />
|
||||
{debugStore.isDebugMode ? "End" : "Start Debugging"}
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -115,15 +142,17 @@ function WorkflowHeader({
|
||||
<ChevronDownIcon className="h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
navigate(`/workflows/${workflowPermanentId}/run`);
|
||||
}}
|
||||
>
|
||||
<PlayIcon className="mr-2 h-6 w-6" />
|
||||
Run
|
||||
</Button>
|
||||
{!debugStore.isDebugMode && (
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
navigate(`/workflows/${workflowPermanentId}/run`);
|
||||
}}
|
||||
>
|
||||
<PlayIcon className="mr-2 h-6 w-6" />
|
||||
Run
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
} from "@/components/ui/accordion";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -17,8 +15,6 @@ import {
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import type { ActionNode } from "./types";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -28,14 +24,16 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { placeholders, helpTooltips } from "../../helpContent";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { AppNode } from "..";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
|
||||
const urlTooltip =
|
||||
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
|
||||
@@ -44,13 +42,9 @@ const navigationGoalTooltip =
|
||||
|
||||
const navigationGoalPlaceholder = 'Input {{ name }} into "Name" field.';
|
||||
|
||||
function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { editable, debuggable, label } = data;
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
@@ -64,7 +58,11 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
engine: data.engine,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const debugStore = useDebugStore();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
@@ -94,33 +92,29 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<header className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.Action}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Action Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<div
|
||||
className={cn("space-y-4", {
|
||||
"opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
@@ -169,7 +163,13 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<Accordion type="single" collapsible>
|
||||
<Accordion
|
||||
className={cn({
|
||||
"pointer-events-none opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
type="single"
|
||||
collapsible
|
||||
>
|
||||
<AccordionItem value="advanced" className="border-b-0">
|
||||
<AccordionTrigger className="py-0">
|
||||
Advanced Settings
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { RunEngine } from "@/api/types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type ActionNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
@@ -19,6 +20,7 @@ export type ActionNodeData = NodeBaseData & {
|
||||
export type ActionNode = Node<ActionNodeData, "action">;
|
||||
|
||||
export const actionNodeDefaultData: ActionNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("action"),
|
||||
label: "",
|
||||
url: "",
|
||||
navigationGoal: "",
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { WorkflowBlockInputSet } from "@/components/WorkflowBlockInputSet";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import type { CodeBlockNode } from "./types";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
code: data.code,
|
||||
parameterKeys: data.parameterKeys,
|
||||
@@ -37,32 +36,24 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.Code}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Code Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="code" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-slate-300">Input Parameters</Label>
|
||||
<WorkflowBlockInputSet
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type CodeBlockNodeData = NodeBaseData & {
|
||||
code: string;
|
||||
@@ -20,6 +21,7 @@ const codeLead = `
|
||||
`;
|
||||
|
||||
export const codeBlockNodeDefaultData: CodeBlockNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("code"),
|
||||
editable: true,
|
||||
label: "",
|
||||
code: codeLead,
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import type { DownloadNode } from "./types";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -32,32 +31,24 @@ function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.DownloadToS3}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Download Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="file_download" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { SKYVERN_DOWNLOAD_DIRECTORY } from "../../constants";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type DownloadNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
@@ -9,6 +10,7 @@ export type DownloadNodeData = NodeBaseData & {
|
||||
export type DownloadNode = Node<DownloadNodeData, "download">;
|
||||
|
||||
export const downloadNodeDefaultData: DownloadNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("download_to_s3"),
|
||||
editable: true,
|
||||
label: "",
|
||||
url: SKYVERN_DOWNLOAD_DIRECTORY,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { ExtractIcon } from "@/components/icons/ExtractIcon";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -10,8 +9,6 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -21,8 +18,6 @@ import {
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { dataSchemaExampleValue } from "../types";
|
||||
import type { ExtractionNode } from "./types";
|
||||
|
||||
@@ -35,14 +30,19 @@ import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function ExtractionNode({ id, data }: NodeProps<ExtractionNode>) {
|
||||
function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
dataExtractionGoal: data.dataExtractionGoal,
|
||||
@@ -53,7 +53,6 @@ function ExtractionNode({ id, data }: NodeProps<ExtractionNode>) {
|
||||
engine: data.engine,
|
||||
model: data.model,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
@@ -82,29 +81,24 @@ function ExtractionNode({ id, data }: NodeProps<ExtractionNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<header className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<ExtractIcon className="size-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Extraction Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type={type}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { RunEngine } from "@/api/types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type ExtractionNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
@@ -16,6 +17,7 @@ export type ExtractionNodeData = NodeBaseData & {
|
||||
export type ExtractionNode = Node<ExtractionNodeData, "extraction">;
|
||||
|
||||
export const extractionNodeDefaultData: ExtractionNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("extraction"),
|
||||
label: "",
|
||||
url: "",
|
||||
dataExtractionGoal: "",
|
||||
|
||||
@@ -12,9 +12,6 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { DownloadIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -25,8 +22,6 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import type { FileDownloadNode } from "./types";
|
||||
import { AppNode } from "..";
|
||||
@@ -35,6 +30,10 @@ import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const urlTooltip =
|
||||
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
|
||||
@@ -45,11 +44,12 @@ const navigationGoalPlaceholder = "Tell Skyvern which file to download.";
|
||||
|
||||
function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
@@ -63,14 +63,10 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
engine: data.engine,
|
||||
model: data.model,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
@@ -93,31 +89,24 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<header className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<DownloadIcon className="size-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">
|
||||
File Download Block
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
type="file_download" // sic: the naming for this block is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { RunEngine } from "@/api/types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type FileDownloadNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
@@ -19,6 +20,7 @@ export type FileDownloadNodeData = NodeBaseData & {
|
||||
export type FileDownloadNode = Node<FileDownloadNodeData, "fileDownload">;
|
||||
|
||||
export const fileDownloadNodeDefaultData: FileDownloadNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("file_download"),
|
||||
label: "",
|
||||
url: "",
|
||||
navigationGoal: "",
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { type FileParserNode } from "./types";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
fileUrl: data.fileUrl,
|
||||
});
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
@@ -40,32 +39,24 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.FileURLParser}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">File Parser Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="file_url_parser" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type FileParserNodeData = NodeBaseData & {
|
||||
fileUrl: string;
|
||||
@@ -8,6 +9,7 @@ export type FileParserNodeData = NodeBaseData & {
|
||||
export type FileParserNode = Node<FileParserNodeData, "fileParser">;
|
||||
|
||||
export const fileParserNodeDefaultData: FileParserNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("file_url_parser"),
|
||||
editable: true,
|
||||
label: "",
|
||||
fileUrl: "",
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { type FileUploadNode } from "./types";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { useState } from "react";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
storageType: data.storageType,
|
||||
@@ -52,32 +51,24 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.UploadToS3}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">File Upload Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="file_upload" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type FileUploadNodeData = NodeBaseData & {
|
||||
path: string;
|
||||
@@ -14,6 +15,7 @@ export type FileUploadNodeData = NodeBaseData & {
|
||||
export type FileUploadNode = Node<FileUploadNodeData, "fileUpload">;
|
||||
|
||||
export const fileUploadNodeDefaultData: FileUploadNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("upload_to_s3"),
|
||||
editable: true,
|
||||
storageType: "s3",
|
||||
label: "",
|
||||
|
||||
@@ -12,9 +12,6 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -25,10 +22,7 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import type { LoginNode } from "./types";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { AppNode } from "..";
|
||||
@@ -37,14 +31,19 @@ import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"
|
||||
import { LoginBlockCredentialSelector } from "./LoginBlockCredentialSelector";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function LoginNode({ id, data }: NodeProps<LoginNode>) {
|
||||
function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
@@ -59,7 +58,6 @@ function LoginNode({ id, data }: NodeProps<LoginNode>) {
|
||||
engine: data.engine,
|
||||
model: data.model,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
@@ -88,32 +86,24 @@ function LoginNode({ id, data }: NodeProps<LoginNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<header className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.Login}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Login Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { RunEngine } from "@/api/types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type LoginNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
@@ -20,6 +21,7 @@ export type LoginNodeData = NodeBaseData & {
|
||||
export type LoginNode = Node<LoginNodeData, "login">;
|
||||
|
||||
export const loginNodeDefaultData: LoginNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("login"),
|
||||
label: "",
|
||||
url: "",
|
||||
navigationGoal:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import type { Node } from "@xyflow/react";
|
||||
import {
|
||||
Handle,
|
||||
@@ -14,14 +11,15 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { AppNode } from "..";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import type { LoopNode } from "./types";
|
||||
import { useState } from "react";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { getLoopNodeWidth } from "../../workflowEditorUtils";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
@@ -30,14 +28,15 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
if (!node) {
|
||||
throw new Error("Node not found"); // not possible
|
||||
}
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
loopVariableReference: data.loopVariableReference,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
@@ -91,32 +90,24 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.ForLoop}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Loop Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="for_loop" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type LoopNodeData = NodeBaseData & {
|
||||
loopValue: string;
|
||||
@@ -10,6 +11,7 @@ export type LoopNodeData = NodeBaseData & {
|
||||
export type LoopNode = Node<LoopNodeData, "loop">;
|
||||
|
||||
export const loopNodeDefaultData: LoopNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("for_loop"),
|
||||
editable: true,
|
||||
label: "",
|
||||
loopValue: "",
|
||||
|
||||
@@ -13,9 +13,6 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -26,10 +23,7 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import type { NavigationNode } from "./types";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { AppNode } from "..";
|
||||
@@ -37,32 +31,36 @@ import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
|
||||
function NavigationNode({ id, data }: NodeProps<NavigationNode>) {
|
||||
function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const debugStore = useDebugStore();
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { editable, debuggable, label } = data;
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
maxStepsOverride: data.maxStepsOverride,
|
||||
allowDownloads: data.allowDownloads,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
cacheActions: data.cacheActions,
|
||||
downloadSuffix: data.downloadSuffix,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
completeCriterion: data.completeCriterion,
|
||||
terminateCriterion: data.terminateCriterion,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
downloadSuffix: data.downloadSuffix,
|
||||
engine: data.engine,
|
||||
model: data.model,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
includeActionHistoryInVerification: data.includeActionHistoryInVerification,
|
||||
maxStepsOverride: data.maxStepsOverride,
|
||||
model: data.model,
|
||||
navigationGoal: data.navigationGoal,
|
||||
terminateCriterion: data.terminateCriterion,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
url: data.url,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
@@ -92,33 +90,29 @@ function NavigationNode({ id, data }: NodeProps<NavigationNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<header className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.Navigation}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Navigation Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
disabled={elideFromDebugging}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<div
|
||||
className={cn("space-y-4", {
|
||||
"opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
@@ -170,7 +164,13 @@ function NavigationNode({ id, data }: NodeProps<NavigationNode>) {
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<Accordion type="single" collapsible>
|
||||
<Accordion
|
||||
className={cn({
|
||||
"pointer-events-none opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
type="single"
|
||||
collapsible
|
||||
>
|
||||
<AccordionItem value="advanced" className="border-b-0">
|
||||
<AccordionTrigger className="py-0">
|
||||
Advanced Settings
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { RunEngine } from "@/api/types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type NavigationNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
@@ -23,6 +24,7 @@ export type NavigationNodeData = NodeBaseData & {
|
||||
export type NavigationNode = Node<NavigationNodeData, "navigation">;
|
||||
|
||||
export const navigationNodeDefaultData: NavigationNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("navigation"),
|
||||
label: "",
|
||||
url: "",
|
||||
navigationGoal: "",
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { dataSchemaExampleForFileExtraction } from "../types";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { type PDFParserNode } from "./types";
|
||||
import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
fileUrl: data.fileUrl,
|
||||
jsonSchema: data.jsonSchema,
|
||||
});
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!data.editable) {
|
||||
@@ -51,32 +50,24 @@ function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.PDFParser}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">PDF Parser Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="pdf_parser" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { AppNode } from "..";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type PDFParserNodeData = NodeBaseData & {
|
||||
fileUrl: string;
|
||||
@@ -10,6 +11,7 @@ export type PDFParserNodeData = NodeBaseData & {
|
||||
export type PDFParserNode = Node<PDFParserNodeData, "pdfParser">;
|
||||
|
||||
export const pdfParserNodeDefaultData: PDFParserNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("pdf_parser"),
|
||||
editable: true,
|
||||
label: "",
|
||||
fileUrl: "",
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { type SendEmailNode } from "./types";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
recipients: data.recipients,
|
||||
subject: data.subject,
|
||||
@@ -53,32 +52,24 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.SendEmail}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Send Email Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="send_email" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label className="text-xs text-slate-300">Recipients</Label>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SMTP_USERNAME_PARAMETER_KEY,
|
||||
} from "../../constants";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type SendEmailNodeData = NodeBaseData & {
|
||||
recipients: string;
|
||||
@@ -24,6 +25,7 @@ export type SendEmailNodeData = NodeBaseData & {
|
||||
export type SendEmailNode = Node<SendEmailNodeData, "sendEmail">;
|
||||
|
||||
export const sendEmailNodeDefaultData: SendEmailNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("send_email"),
|
||||
recipients: "",
|
||||
subject: "",
|
||||
body: "",
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -22,8 +22,10 @@ import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { WorkflowModel } from "@/routes/workflows/types/workflowTypes";
|
||||
import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "../Taskv2Node/types";
|
||||
import { KeyValueInput } from "@/components/KeyValueInput";
|
||||
import { useWorkflowSettingsStore } from "@/store/WorkflowSettingsStore";
|
||||
|
||||
function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
const workflowSettingsStore = useWorkflowSettingsStore();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const { updateNodeData } = useReactFlow();
|
||||
|
||||
@@ -59,6 +61,11 @@ function StartNode({ id, data }: NodeProps<StartNode>) {
|
||||
extraHttpHeaders: data.withWorkflowSettings ? data.extraHttpHeaders : null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
workflowSettingsStore.setWorkflowSettings(inputs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputs]);
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
|
||||
@@ -13,9 +13,6 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -28,31 +25,31 @@ import { useState } from "react";
|
||||
import { AppNode } from "..";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { dataSchemaExampleValue, errorMappingExampleValue } from "../types";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { ParametersMultiSelect } from "./ParametersMultiSelect";
|
||||
import type { TaskNode } from "./types";
|
||||
import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { RunEngineSelector } from "@/components/EngineSelector";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function TaskNode({ id, data }: NodeProps<TaskNode>) {
|
||||
function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
@@ -95,32 +92,24 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-2 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.Task}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Task Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<Accordion type="multiple" defaultValue={["content", "extraction"]}>
|
||||
<AccordionItem value="content">
|
||||
<AccordionTrigger>Content</AccordionTrigger>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { RunEngine } from "@/api/types";
|
||||
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
export type TaskNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
navigationGoal: string;
|
||||
@@ -25,6 +25,7 @@ export type TaskNodeData = NodeBaseData & {
|
||||
export type TaskNode = Node<TaskNodeData, "task">;
|
||||
|
||||
export const taskNodeDefaultData: TaskNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("task"),
|
||||
url: "",
|
||||
navigationGoal: "",
|
||||
dataExtractionGoal: "",
|
||||
|
||||
@@ -9,28 +9,25 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips, placeholders } from "../../helpContent";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { MAX_STEPS_DEFAULT, type Taskv2Node } from "./types";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -64,34 +61,24 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.Taskv2}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">
|
||||
Navigation v2 Block
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
type="task_v2" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export const MAX_STEPS_DEFAULT = 25;
|
||||
export const MAX_SCREENSHOT_SCROLLS_DEFAULT = 3;
|
||||
@@ -16,6 +17,7 @@ export type Taskv2NodeData = NodeBaseData & {
|
||||
export type Taskv2Node = Node<Taskv2NodeData, "taskv2">;
|
||||
|
||||
export const taskv2NodeDefaultData: Taskv2NodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("task_v2"),
|
||||
label: "",
|
||||
continueOnFailure: false,
|
||||
editable: true,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -16,21 +13,26 @@ import { useState } from "react";
|
||||
import { AppNode } from "..";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { type TextPromptNode } from "./types";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup";
|
||||
import { dataSchemaExampleValue } from "../types";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
prompt: data.prompt,
|
||||
jsonSchema: data.jsonSchema,
|
||||
@@ -41,11 +43,6 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
return;
|
||||
@@ -70,32 +67,24 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.TextPrompt}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Text Prompt Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="text_prompt" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { AppNode } from "..";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type TextPromptNodeData = NodeBaseData & {
|
||||
prompt: string;
|
||||
@@ -11,6 +12,7 @@ export type TextPromptNodeData = NodeBaseData & {
|
||||
export type TextPromptNode = Node<TextPromptNodeData, "textPrompt">;
|
||||
|
||||
export const textPromptNodeDefaultData: TextPromptNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("text_prompt"),
|
||||
editable: true,
|
||||
label: "",
|
||||
prompt: "",
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import type { URLNode } from "./types";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { useState } from "react";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { placeholders } from "../../helpContent";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function URLNode({ id, data, type }: NodeProps<URLNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -48,32 +46,24 @@ function URLNode({ id, data, type }: NodeProps<URLNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.URL}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Go to URL Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="goto_url"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type URLNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
@@ -8,6 +9,7 @@ export type URLNodeData = NodeBaseData & {
|
||||
export type URLNode = Node<URLNodeData, "url">;
|
||||
|
||||
export const urlNodeDefaultData: URLNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("goto_url"),
|
||||
label: "",
|
||||
continueOnFailure: false,
|
||||
url: "",
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { type UploadNode } from "./types";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function UploadNode({ id, data }: NodeProps<UploadNode>) {
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -32,32 +31,24 @@ function UploadNode({ id, data }: NodeProps<UploadNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.UploadToS3}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={data.editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Upload Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type="upload_to_s3" // sic: the naming is not consistent
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { SKYVERN_DOWNLOAD_DIRECTORY } from "../../constants";
|
||||
import { NodeBaseData } from "../types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export type UploadNodeData = NodeBaseData & {
|
||||
path: string;
|
||||
@@ -10,6 +11,7 @@ export type UploadNodeData = NodeBaseData & {
|
||||
export type UploadNode = Node<UploadNodeData, "upload">;
|
||||
|
||||
export const uploadNodeDefaultData: UploadNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("file_upload"),
|
||||
editable: true,
|
||||
label: "",
|
||||
path: SKYVERN_DOWNLOAD_DIRECTORY,
|
||||
|
||||
@@ -11,9 +11,6 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -24,31 +21,32 @@ import {
|
||||
} from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import type { ValidationNode } from "./types";
|
||||
import { AppNode } from "..";
|
||||
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
|
||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { ModelSelector } from "@/components/ModelSelector";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function ValidationNode({ id, data }: NodeProps<ValidationNode>) {
|
||||
function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
completeCriterion: data.completeCriterion,
|
||||
terminateCriterion: data.terminateCriterion,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
model: data.model,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const edges = useEdges();
|
||||
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
|
||||
@@ -77,32 +75,24 @@ function ValidationNode({ id, data }: NodeProps<ValidationNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<header className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.Validation}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Validation Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type={type}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Label className="text-xs text-slate-300">Complete if...</Label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
export type ValidationNodeData = NodeBaseData & {
|
||||
completeCriterion: string;
|
||||
terminateCriterion: string;
|
||||
@@ -11,6 +11,7 @@ export type ValidationNodeData = NodeBaseData & {
|
||||
export type ValidationNode = Node<ValidationNodeData, "validation">;
|
||||
|
||||
export const validationNodeDefaultData: ValidationNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("validation"),
|
||||
label: "",
|
||||
completeCriterion: "",
|
||||
terminateCriterion: "",
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { helpTooltips } from "../../helpContent";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import type { WaitNode } from "./types";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import { NodeHeader } from "../components/NodeHeader";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function WaitNode({ id, data }: NodeProps<WaitNode>) {
|
||||
function WaitNode({ id, data, type }: NodeProps<WaitNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const { debuggable, editable, label } = data;
|
||||
const debugStore = useDebugStore();
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const [inputs, setInputs] = useState({
|
||||
waitInSeconds: data.waitInSeconds,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
|
||||
function handleChange(key: string, value: unknown) {
|
||||
if (!editable) {
|
||||
@@ -49,32 +47,24 @@ function WaitNode({ id, data }: NodeProps<WaitNode>) {
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<header className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={WorkflowBlockTypes.Wait}
|
||||
className="size-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Wait Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div
|
||||
className={cn(
|
||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
{
|
||||
"pointer-events-none bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
disabled={elideFromDebugging}
|
||||
editable={editable}
|
||||
nodeId={id}
|
||||
totpIdentifier={null}
|
||||
totpUrl={null}
|
||||
type={type}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
export type WaitNodeData = NodeBaseData & {
|
||||
waitInSeconds: string;
|
||||
};
|
||||
@@ -8,6 +8,7 @@ export type WaitNodeData = NodeBaseData & {
|
||||
export type WaitNode = Node<WaitNodeData, "wait">;
|
||||
|
||||
export const waitNodeDefaultData: WaitNodeData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has("wait"),
|
||||
label: "",
|
||||
continueOnFailure: false,
|
||||
editable: true,
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { ReloadIcon, PlayIcon, StopIcon } from "@radix-ui/react-icons";
|
||||
import { useEffect } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import { Timer } from "@/components/Timer";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import {
|
||||
debuggableWorkflowBlockTypes,
|
||||
type WorkflowBlockType,
|
||||
type WorkflowApiResponse,
|
||||
} from "@/routes/workflows/types/workflowTypes";
|
||||
import { getInitialValues } from "@/routes/workflows/utils";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import {
|
||||
useWorkflowSettingsStore,
|
||||
type WorkflowSettingsState,
|
||||
} from "@/store/WorkflowSettingsStore";
|
||||
import { cn } from "@/util/utils";
|
||||
import {
|
||||
statusIsAFailureType,
|
||||
statusIsFinalized,
|
||||
statusIsRunningOrQueued,
|
||||
} from "@/routes/tasks/types";
|
||||
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||
import { lsKeys } from "@/util/env";
|
||||
|
||||
interface Props {
|
||||
blockLabel: string; // today, this + wpid act as the identity of a block
|
||||
disabled?: boolean;
|
||||
editable: boolean;
|
||||
nodeId: string;
|
||||
totpIdentifier: string | null;
|
||||
totpUrl: string | null;
|
||||
type: WorkflowBlockType;
|
||||
}
|
||||
|
||||
type Payload = Record<string, unknown> & {
|
||||
block_labels: string[];
|
||||
browser_session_id: string | null;
|
||||
extra_http_headers: Record<string, string> | null;
|
||||
max_screenshot_scrolls: number | null;
|
||||
parameters: Record<string, unknown>;
|
||||
proxy_location: ProxyLocation;
|
||||
totp_identifier: string | null;
|
||||
totp_url: string | null;
|
||||
webhook_url: string | null;
|
||||
workflow_id: string;
|
||||
};
|
||||
|
||||
const blockTypeToTitle = (type: WorkflowBlockType): string => {
|
||||
const parts = type.split("_");
|
||||
const capCased = parts
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
|
||||
return `${capCased} Block`;
|
||||
};
|
||||
|
||||
const getPayload = (opts: {
|
||||
blockLabel: string;
|
||||
parameters: Record<string, unknown>;
|
||||
totpIdentifier: string | null;
|
||||
totpUrl: string | null;
|
||||
workflowPermanentId: string;
|
||||
workflowSettings: WorkflowSettingsState;
|
||||
}): Payload => {
|
||||
const webhook_url = opts.workflowSettings.webhookCallbackUrl.trim();
|
||||
|
||||
let extraHttpHeaders = null;
|
||||
|
||||
try {
|
||||
extraHttpHeaders =
|
||||
opts.workflowSettings.extraHttpHeaders === null
|
||||
? null
|
||||
: JSON.parse(opts.workflowSettings.extraHttpHeaders);
|
||||
} catch (e: unknown) {
|
||||
toast({
|
||||
variant: "warning",
|
||||
title: "Extra HTTP Headers",
|
||||
description: "Invalid extra HTTP Headers JSON",
|
||||
});
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem(lsKeys.optimisticBrowserSession);
|
||||
let browserSessionId: string | null = null;
|
||||
try {
|
||||
const parsed = JSON.parse(stored ?? "");
|
||||
const { browser_session_id } = parsed;
|
||||
browserSessionId = browser_session_id as string;
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
|
||||
if (!browserSessionId) {
|
||||
toast({
|
||||
variant: "warning",
|
||||
title: "Error",
|
||||
description: "No browser session ID found",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Success",
|
||||
description: `Browser session ID found: ${browserSessionId}`,
|
||||
});
|
||||
}
|
||||
|
||||
const payload: Payload = {
|
||||
block_labels: [opts.blockLabel],
|
||||
browser_session_id: browserSessionId,
|
||||
extra_http_headers: extraHttpHeaders,
|
||||
max_screenshot_scrolls: opts.workflowSettings.maxScreenshotScrollingTimes,
|
||||
parameters: opts.parameters,
|
||||
proxy_location: opts.workflowSettings.proxyLocation,
|
||||
totp_identifier: opts.totpIdentifier,
|
||||
totp_url: opts.totpUrl,
|
||||
webhook_url: webhook_url.length > 0 ? webhook_url : null,
|
||||
workflow_id: opts.workflowPermanentId,
|
||||
};
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
function NodeHeader({
|
||||
blockLabel,
|
||||
disabled = false,
|
||||
editable,
|
||||
nodeId,
|
||||
totpIdentifier,
|
||||
totpUrl,
|
||||
type,
|
||||
}: Props) {
|
||||
const {
|
||||
blockLabel: urlBlockLabel,
|
||||
workflowPermanentId,
|
||||
workflowRunId,
|
||||
} = useParams();
|
||||
const debugStore = useDebugStore();
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === blockLabel;
|
||||
const anyBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel.length > 0;
|
||||
const workflowSettingsStore = useWorkflowSettingsStore();
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id: nodeId,
|
||||
initialValue: blockLabel,
|
||||
});
|
||||
const blockTitle = blockTypeToTitle(type);
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const location = useLocation();
|
||||
const isDebuggable = debuggableWorkflowBlockTypes.has(type);
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
const workflowRunIsRunningOrQueued =
|
||||
workflowRun && statusIsRunningOrQueued(workflowRun);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workflowRun || !workflowPermanentId || !workflowRunId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
workflowRunId === workflowRun?.workflow_run_id &&
|
||||
statusIsFinalized(workflowRun)
|
||||
) {
|
||||
navigate(`/workflows/${workflowPermanentId}/debug`);
|
||||
|
||||
if (statusIsAFailureType(workflowRun)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: `Workflow Block ${urlBlockLabel}: ${workflowRun.status}`,
|
||||
description: `Reason: ${workflowRun.failure_reason}`,
|
||||
});
|
||||
} else if (statusIsFinalized(workflowRun)) {
|
||||
toast({
|
||||
variant: "success",
|
||||
title: `Workflow Block ${urlBlockLabel}: ${workflowRun.status}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
urlBlockLabel,
|
||||
navigate,
|
||||
workflowPermanentId,
|
||||
workflowRun,
|
||||
workflowRunId,
|
||||
]);
|
||||
|
||||
const runBlock = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!workflowPermanentId) {
|
||||
console.error("There is no workflowPermanentId");
|
||||
return;
|
||||
}
|
||||
|
||||
const workflow = await queryClient.fetchQuery<WorkflowApiResponse>({
|
||||
queryKey: ["block", "workflow", workflowPermanentId],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
return client
|
||||
.get(`/workflows/${workflowPermanentId}`)
|
||||
.then((response) => response.data);
|
||||
},
|
||||
});
|
||||
|
||||
const workflowParameters =
|
||||
workflow?.workflow_definition.parameters.filter(
|
||||
(parameter) => parameter.parameter_type === "workflow",
|
||||
);
|
||||
|
||||
const parameters = getInitialValues(location, workflowParameters ?? []);
|
||||
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
|
||||
const body = getPayload({
|
||||
blockLabel,
|
||||
parameters,
|
||||
totpIdentifier,
|
||||
totpUrl,
|
||||
workflowPermanentId,
|
||||
workflowSettings: workflowSettingsStore,
|
||||
});
|
||||
|
||||
return await client.post<Payload, { data: { run_id: string } }>(
|
||||
"/run/workflows/blocks",
|
||||
body,
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
if (!response) {
|
||||
console.error("No response");
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
title: "Workflow block run started",
|
||||
description: "The workflow block run has been started successfully",
|
||||
});
|
||||
|
||||
navigate(
|
||||
`/workflows/${workflowPermanentId}/${response.data.run_id}/${label}/debug`,
|
||||
);
|
||||
},
|
||||
onError: (error: AxiosError) => {
|
||||
const detail = (error.response?.data as { detail?: string })?.detail;
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to start workflow block run",
|
||||
description: detail ?? error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const cancelBlock = useMutation({
|
||||
mutationFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
return client
|
||||
.post(`/workflows/runs/${workflowRunId}/cancel`)
|
||||
.then((response) => response.data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
variant: "success",
|
||||
title: "Workflow Canceled",
|
||||
description: "The workflow has been successfully canceled.",
|
||||
});
|
||||
navigate(`/workflows/${workflowPermanentId}/debug`);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleOnPlay = () => {
|
||||
runBlock.mutate();
|
||||
};
|
||||
|
||||
const handleOnCancel = () => {
|
||||
cancelBlock.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{thisBlockIsPlaying && (
|
||||
<div className="flex w-full animate-[auto-height_1s_ease-in-out_forwards] items-center justify-between overflow-hidden">
|
||||
<div className="pb-4">
|
||||
<Timer />
|
||||
</div>
|
||||
<div className="pb-4">{workflowRun?.status ?? "pending"}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header className="!mt-0 flex h-[2.75rem] justify-between gap-2">
|
||||
<div
|
||||
className={cn("flex gap-2", {
|
||||
"opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
>
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<WorkflowBlockIcon workflowBlockType={type} className="size-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={blockLabel}
|
||||
editable={editable}
|
||||
onChange={setLabel}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">{blockTitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-auto ml-auto flex items-center gap-2">
|
||||
{thisBlockIsPlaying && workflowRunIsRunningOrQueued && (
|
||||
<div className="ml-auto">
|
||||
<button className="rounded p-1 hover:bg-red-500 hover:text-black disabled:opacity-50">
|
||||
{cancelBlock.isPending ? (
|
||||
<ReloadIcon className="size-6 animate-spin" />
|
||||
) : (
|
||||
<StopIcon
|
||||
className="size-6"
|
||||
onClick={() => {
|
||||
handleOnCancel();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{debugStore.isDebugMode && isDebuggable && (
|
||||
<button
|
||||
disabled={anyBlockIsPlaying}
|
||||
className={cn("rounded p-1 disabled:opacity-50", {
|
||||
"hover:bg-muted": anyBlockIsPlaying,
|
||||
})}
|
||||
>
|
||||
{runBlock.isPending ? (
|
||||
<ReloadIcon className="size-6 animate-spin" />
|
||||
) : (
|
||||
<PlayIcon
|
||||
className={cn("size-6", {
|
||||
"fill-gray-500 text-gray-500":
|
||||
anyBlockIsPlaying || !workflowPermanentId,
|
||||
})}
|
||||
onClick={() => {
|
||||
handleOnPlay();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{disabled || debugStore.isDebugMode ? null : (
|
||||
<div>
|
||||
<div
|
||||
className={cn("rounded p-1 hover:bg-muted", {
|
||||
"pointer-events-none opacity-50": anyBlockIsPlaying,
|
||||
})}
|
||||
>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(nodeId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { NodeHeader };
|
||||
@@ -2,6 +2,7 @@ import { WorkflowBlockType } from "../../types/workflowTypes";
|
||||
import type { WorkflowModel } from "../../types/workflowTypes";
|
||||
|
||||
export type NodeBaseData = {
|
||||
debuggable: boolean;
|
||||
label: string;
|
||||
continueOnFailure: boolean;
|
||||
editable: boolean;
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* NOTE(jdo): this is not a "panel", in the react-flow sense. It's a floating,
|
||||
* draggable, resizeable window, like on a desktop. But I am putting it here
|
||||
* for now.
|
||||
*/
|
||||
|
||||
import { Resizable } from "re-resizable";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import Draggable from "react-draggable";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
/**
|
||||
* TODO(jdo): extract this to a reusable Window component.
|
||||
*/
|
||||
function WorkflowDebugOverviewWindow() {
|
||||
const debugStore = useDebugStore();
|
||||
const isDebugMode = debugStore.isDebugMode;
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [size, setSize] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 800,
|
||||
height: 680,
|
||||
});
|
||||
const [lastSize, setLastSize] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 800,
|
||||
height: 680,
|
||||
});
|
||||
const [sizeBeforeMaximize, setSizeBeforeMaximize] = useState({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 800,
|
||||
height: 680,
|
||||
});
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const resizableRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [dragStartSize, setDragStartSize] = useState<
|
||||
| {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
const onResize = useCallback(
|
||||
({
|
||||
delta,
|
||||
direction,
|
||||
size,
|
||||
}: {
|
||||
delta: { width: number; height: number };
|
||||
direction: string;
|
||||
size: { left: number; top: number; width: number; height: number };
|
||||
}) => {
|
||||
if (!dragStartSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const top =
|
||||
resizableRef.current?.parentElement?.offsetTop ?? lastSize.top;
|
||||
const left =
|
||||
resizableRef.current?.parentElement?.offsetLeft ?? lastSize.left;
|
||||
const width =
|
||||
resizableRef.current?.parentElement?.offsetWidth ?? lastSize.width;
|
||||
const height =
|
||||
resizableRef.current?.parentElement?.offsetHeight ?? lastSize.height;
|
||||
|
||||
setLastSize({ top, left, width, height });
|
||||
const directions = ["top", "left", "topLeft", "bottomLeft", "topRight"];
|
||||
|
||||
if (directions.indexOf(direction) !== -1) {
|
||||
let newLeft = size.left;
|
||||
let newTop = size.top;
|
||||
|
||||
if (direction === "bottomLeft") {
|
||||
newLeft = dragStartSize.left - delta.width;
|
||||
} else if (direction === "topRight") {
|
||||
newTop = dragStartSize.top - delta.height;
|
||||
} else {
|
||||
newLeft = dragStartSize.left - delta.width;
|
||||
newTop = dragStartSize.top - delta.height;
|
||||
}
|
||||
|
||||
// TODO(follow-up): https://github.com/bokuweb/re-resizable/issues/868
|
||||
flushSync(() => {
|
||||
setSize({
|
||||
...size,
|
||||
left: newLeft,
|
||||
top: newTop,
|
||||
});
|
||||
setPosition({ x: newLeft, y: newTop });
|
||||
});
|
||||
} else {
|
||||
flushSync(() => {
|
||||
setSize({
|
||||
...size,
|
||||
left: size.left,
|
||||
top: size.top,
|
||||
});
|
||||
setPosition({ x: size.left, y: size.top });
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[dragStartSize],
|
||||
);
|
||||
|
||||
/**
|
||||
* Forces the sizing to take place after the resize is complete.
|
||||
*
|
||||
* TODO(jdo): emits warnings in the dev console. ref: https://github.com/bokuweb/re-resizable/issues/868
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isResizing) {
|
||||
return;
|
||||
}
|
||||
const width = lastSize.width;
|
||||
const height = lastSize.height;
|
||||
|
||||
flushSync(() => {
|
||||
setSize({
|
||||
...size,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isResizing]);
|
||||
|
||||
const onDrag = (position: { x: number; y: number }) => {
|
||||
if (isMaximized) {
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
|
||||
setPosition({ x: position.x, y: position.y });
|
||||
|
||||
setSize({
|
||||
...size,
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
});
|
||||
|
||||
setLastSize({
|
||||
...size,
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
});
|
||||
};
|
||||
|
||||
const onDblClickHeader = () => {
|
||||
if (!isMaximized) {
|
||||
maximize();
|
||||
} else {
|
||||
restore();
|
||||
}
|
||||
};
|
||||
|
||||
const maximize = () => {
|
||||
const parent = parentRef.current;
|
||||
|
||||
if (!parent) {
|
||||
console.warn("No parent - cannot maximize.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSizeBeforeMaximize({
|
||||
...size,
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
});
|
||||
|
||||
setIsMaximized(true);
|
||||
|
||||
setSize({
|
||||
left: 0,
|
||||
top: 0,
|
||||
// has to take into account padding...hack
|
||||
width: parent.offsetWidth - 16,
|
||||
height: parent.offsetHeight - 16,
|
||||
});
|
||||
|
||||
setPosition({ x: 0, y: 0 });
|
||||
};
|
||||
|
||||
const restore = () => {
|
||||
const restoreSize = sizeBeforeMaximize;
|
||||
|
||||
const position = isDragging
|
||||
? { left: 0, top: 0 }
|
||||
: {
|
||||
left: restoreSize.left,
|
||||
top: restoreSize.top,
|
||||
};
|
||||
|
||||
setSize({
|
||||
left: position.left,
|
||||
top: position.top,
|
||||
width: restoreSize.width,
|
||||
height: restoreSize.height,
|
||||
});
|
||||
|
||||
setPosition({ x: position.left, y: position.top });
|
||||
|
||||
setIsMaximized(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* If maximized, need to retain max size during parent resizing.
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
const parent = parentRef.current;
|
||||
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMaximized) {
|
||||
setSize({
|
||||
left: 0,
|
||||
top: 0,
|
||||
// has to take into account padding...hack
|
||||
width: parent.offsetWidth - 16,
|
||||
height: parent.offsetHeight - 16,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(parentRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [isMaximized]);
|
||||
|
||||
return !isDebugMode ? null : (
|
||||
<div
|
||||
ref={parentRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
background: "transparent",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
padding: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<Draggable
|
||||
handle=".my-panel-header"
|
||||
position={position}
|
||||
onStart={() => setIsDragging(true)}
|
||||
onDrag={(_, data) => onDrag(data)}
|
||||
onStop={() => setIsDragging(false)}
|
||||
bounds="parent"
|
||||
disabled={isResizing}
|
||||
>
|
||||
<Resizable
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#020817",
|
||||
boxSizing: "border-box",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
className={cn("border-8 border-gray-900", {
|
||||
"hover:border-slate-500": !isMaximized,
|
||||
})}
|
||||
bounds={parentRef.current ?? "parent"}
|
||||
enable={
|
||||
isMaximized
|
||||
? false
|
||||
: {
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
topRight: true,
|
||||
bottomRight: true,
|
||||
bottomLeft: true,
|
||||
topLeft: true,
|
||||
}
|
||||
}
|
||||
onResizeStart={() => {
|
||||
if (isMaximized) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsResizing(true);
|
||||
setDragStartSize({ ...size, left: position.x, top: position.y });
|
||||
}}
|
||||
onResize={(_, direction, __, delta) => {
|
||||
if (isMaximized) {
|
||||
return;
|
||||
}
|
||||
|
||||
onResize({ delta, direction, size });
|
||||
}}
|
||||
onResizeStop={() => {
|
||||
if (isMaximized) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsResizing(false);
|
||||
setDragStartSize(undefined);
|
||||
}}
|
||||
defaultSize={size}
|
||||
size={size}
|
||||
>
|
||||
<div
|
||||
ref={resizableRef}
|
||||
className="my-panel"
|
||||
style={{
|
||||
pointerEvents: "auto",
|
||||
padding: "0px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
onDblClickHeader();
|
||||
}}
|
||||
>
|
||||
<div className="my-panel-header w-full cursor-move bg-[#031827] p-3">
|
||||
Live View
|
||||
</div>
|
||||
<WorkflowDebugOverviewWindowIframe />
|
||||
</div>
|
||||
</Resizable>
|
||||
</Draggable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowDebugOverviewWindowIframe() {
|
||||
const { workflowPermanentId: wpid, workflowRunId: wrid } = useParams();
|
||||
const lastCompletePair = useRef<{ wpid: string; wrid: string } | null>(null);
|
||||
|
||||
if (wpid !== undefined && wrid !== undefined) {
|
||||
lastCompletePair.current = {
|
||||
wpid,
|
||||
wrid,
|
||||
};
|
||||
}
|
||||
|
||||
const paramsToUse = useMemo(() => {
|
||||
if (wpid && wrid) {
|
||||
return { wpid, wrid };
|
||||
}
|
||||
return lastCompletePair.current;
|
||||
}, [wpid, wrid]);
|
||||
|
||||
const origin = location.origin;
|
||||
const dest = paramsToUse
|
||||
? `${origin}/workflows/${paramsToUse.wpid}/${paramsToUse.wrid}/overview?embed=true`
|
||||
: null;
|
||||
|
||||
return dest ? (
|
||||
<div className="h-full w-full rounded-xl bg-[#020817] p-6">
|
||||
<iframe src={dest} className="h-full w-full rounded-xl" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full rounded-xl bg-[#020817] p-6">
|
||||
<p>Workflow not found</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowDebugOverviewWindow };
|
||||
@@ -99,6 +99,8 @@ import {
|
||||
import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types";
|
||||
import { urlNodeDefaultData } from "./nodes/URLNode/types";
|
||||
import { fileUploadNodeDefaultData } from "./nodes/FileUploadNode/types";
|
||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||
|
||||
function layoutUtil(
|
||||
@@ -205,6 +207,7 @@ function convertToNode(
|
||||
connectable: false,
|
||||
};
|
||||
const commonData: NodeBaseData = {
|
||||
debuggable: debuggableWorkflowBlockTypes.has(block.block_type),
|
||||
label: block.label,
|
||||
continueOnFailure: block.continue_on_failure,
|
||||
editable,
|
||||
|
||||
@@ -214,6 +214,18 @@ export const WorkflowBlockTypes = {
|
||||
URL: "goto_url",
|
||||
} as const;
|
||||
|
||||
export const debuggableWorkflowBlockTypes: Set<WorkflowBlockType> = new Set([
|
||||
"action",
|
||||
"extraction",
|
||||
"goto_url",
|
||||
"login",
|
||||
"navigation",
|
||||
"task",
|
||||
"task_v2",
|
||||
"text_prompt",
|
||||
"validation",
|
||||
]);
|
||||
|
||||
export function isTaskVariantBlock(item: {
|
||||
block_type: WorkflowBlockType;
|
||||
}): boolean {
|
||||
|
||||
49
skyvern-frontend/src/routes/workflows/utils.ts
Normal file
49
skyvern-frontend/src/routes/workflows/utils.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import type { WorkflowParameter } from "./types/workflowTypes";
|
||||
|
||||
type Location = ReturnType<typeof useLocation>;
|
||||
|
||||
export const getInitialValues = (
|
||||
location: Location,
|
||||
workflowParameters: WorkflowParameter[],
|
||||
) => {
|
||||
const iv = location.state?.data
|
||||
? location.state.data
|
||||
: workflowParameters?.reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.workflow_parameter_type === "json") {
|
||||
if (typeof curr.default_value === "string") {
|
||||
acc[curr.key] = curr.default_value;
|
||||
return acc;
|
||||
}
|
||||
if (curr.default_value) {
|
||||
acc[curr.key] = JSON.stringify(curr.default_value, null, 2);
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
if (
|
||||
curr.default_value &&
|
||||
curr.workflow_parameter_type === "boolean"
|
||||
) {
|
||||
acc[curr.key] = Boolean(curr.default_value);
|
||||
return acc;
|
||||
}
|
||||
if (
|
||||
curr.default_value === null &&
|
||||
curr.workflow_parameter_type === "string"
|
||||
) {
|
||||
acc[curr.key] = "";
|
||||
return acc;
|
||||
}
|
||||
if (curr.default_value) {
|
||||
acc[curr.key] = curr.default_value;
|
||||
return acc;
|
||||
}
|
||||
acc[curr.key] = null;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return iv as Record<string, unknown>;
|
||||
};
|
||||
30
skyvern-frontend/src/store/DebugStoreContext.tsx
Normal file
30
skyvern-frontend/src/store/DebugStoreContext.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { createContext, useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
function useIsDebugMode() {
|
||||
const location = useLocation();
|
||||
return useMemo(
|
||||
() => location.pathname.includes("debug"),
|
||||
[location.pathname],
|
||||
);
|
||||
}
|
||||
|
||||
export type DebugStoreContextType = {
|
||||
isDebugMode: boolean;
|
||||
};
|
||||
|
||||
export const DebugStoreContext = createContext<
|
||||
DebugStoreContextType | undefined
|
||||
>(undefined);
|
||||
|
||||
export const DebugStoreProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const isDebugMode = useIsDebugMode();
|
||||
|
||||
return (
|
||||
<DebugStoreContext.Provider value={{ isDebugMode }}>
|
||||
{children}
|
||||
</DebugStoreContext.Provider>
|
||||
);
|
||||
};
|
||||
40
skyvern-frontend/src/store/WorkflowSettingsStore.ts
Normal file
40
skyvern-frontend/src/store/WorkflowSettingsStore.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { create } from "zustand";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
|
||||
export interface WorkflowModel {
|
||||
model_name: string;
|
||||
}
|
||||
|
||||
export interface WorkflowSettingsState {
|
||||
webhookCallbackUrl: string;
|
||||
proxyLocation: ProxyLocation;
|
||||
persistBrowserSession: boolean;
|
||||
model: WorkflowModel | null;
|
||||
maxScreenshotScrollingTimes: number | null;
|
||||
extraHttpHeaders: string | null;
|
||||
setWorkflowSettings: (
|
||||
settings: Partial<Omit<WorkflowSettingsState, "setWorkflowSettings">>,
|
||||
) => void;
|
||||
resetWorkflowSettings: () => void;
|
||||
}
|
||||
|
||||
const defaultState: Omit<
|
||||
WorkflowSettingsState,
|
||||
"setWorkflowSettings" | "resetWorkflowSettings"
|
||||
> = {
|
||||
webhookCallbackUrl: "",
|
||||
proxyLocation: ProxyLocation.Residential,
|
||||
persistBrowserSession: false,
|
||||
model: null,
|
||||
maxScreenshotScrollingTimes: null,
|
||||
extraHttpHeaders: null,
|
||||
};
|
||||
|
||||
export const useWorkflowSettingsStore = create<WorkflowSettingsState>(
|
||||
(set) => ({
|
||||
...defaultState,
|
||||
setWorkflowSettings: (settings) =>
|
||||
set((state) => ({ ...state, ...settings })),
|
||||
resetWorkflowSettings: () => set({ ...defaultState }),
|
||||
}),
|
||||
);
|
||||
10
skyvern-frontend/src/store/useDebugStore.ts
Normal file
10
skyvern-frontend/src/store/useDebugStore.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
import { DebugStoreContext } from "./DebugStoreContext";
|
||||
|
||||
export function useDebugStore() {
|
||||
const ctx = useContext(DebugStoreContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useDebugStore must be used within a DebugStoreProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import { AxiosInstance } from "axios";
|
||||
import { lsKeys } from "@/util/env";
|
||||
|
||||
export interface BrowserSessionData {
|
||||
browser_session_id: string | null;
|
||||
@@ -10,7 +11,6 @@ interface OptimisticBrowserSessionIdState extends BrowserSessionData {
|
||||
run: (client: AxiosInstance) => Promise<BrowserSessionData>;
|
||||
}
|
||||
|
||||
const SESSION_KEY = "skyvern.optimisticBrowserSession";
|
||||
const SESSION_TIMEOUT_MINUTES = 60;
|
||||
|
||||
export const useOptimisticallyRequestBrowserSessionId =
|
||||
@@ -18,7 +18,7 @@ export const useOptimisticallyRequestBrowserSessionId =
|
||||
browser_session_id: null,
|
||||
expires_at: null,
|
||||
run: async (client) => {
|
||||
const stored = localStorage.getItem(SESSION_KEY);
|
||||
const stored = localStorage.getItem(lsKeys.optimisticBrowserSession);
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
@@ -50,7 +50,7 @@ export const useOptimisticallyRequestBrowserSessionId =
|
||||
expires_at: newExpiresAt,
|
||||
});
|
||||
localStorage.setItem(
|
||||
SESSION_KEY,
|
||||
lsKeys.optimisticBrowserSession,
|
||||
JSON.stringify({
|
||||
browser_session_id: newBrowserSessionId,
|
||||
expires_at: newExpiresAt,
|
||||
|
||||
@@ -21,10 +21,16 @@ if (!artifactApiBaseUrl) {
|
||||
|
||||
const apiPathPrefix = import.meta.env.VITE_API_PATH_PREFIX ?? "";
|
||||
|
||||
const lsKeys = {
|
||||
browserSessionId: "skyvern.browserSessionId",
|
||||
optimisticBrowserSession: "skyvern.optimisticBrowserSession",
|
||||
};
|
||||
|
||||
export {
|
||||
apiBaseUrl,
|
||||
environment,
|
||||
envCredential,
|
||||
artifactApiBaseUrl,
|
||||
apiPathPrefix,
|
||||
lsKeys,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user