Jon/sky 5906 debugger show actions inputs outputs for a block run (#3202)

This commit is contained in:
Jonathan Dobson
2025-08-15 07:25:04 -04:00
committed by GitHub
parent 654cdb14e4
commit 40d104c193
33 changed files with 898 additions and 121 deletions

View File

@@ -317,6 +317,7 @@ export type WorkflowRunStatusApiResponse = {
webhook_callback_url: string | null;
extra_http_headers: Record<string, string> | null;
created_at: string;
finished_at: string;
modified_at: string;
parameters: Record<string, unknown>;
screenshot_urls: Array<string> | null;

View File

@@ -7,10 +7,26 @@ interface HMS {
}
interface Props {
override?: number;
startAt?: HMS;
}
function Timer({ startAt }: Props) {
const formatMs = (elapsed: number) => {
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;
return {
hour: hours,
minute: minutes,
second: seconds,
};
};
function Timer({ override, startAt }: Props) {
const [time, setTime] = useState<HMS>({
hour: 0,
minute: 0,
@@ -18,20 +34,22 @@ function Timer({ startAt }: Props) {
});
useEffect(() => {
if (override) {
const formatted = formatMs(override);
setTime(() => formatted);
return;
}
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;
const formatted = formatMs(elapsed);
setTime(() => ({
hour: hours + (startAt?.hour ?? 0),
minute: minutes + (startAt?.minute ?? 0),
second: seconds + (startAt?.second ?? 0),
hour: formatted.hour + (startAt?.hour ?? 0),
minute: formatted.minute + (startAt?.minute ?? 0),
second: formatted.second + (startAt?.second ?? 0),
}));
rAF = requestAnimationFrame(loop);
@@ -40,7 +58,7 @@ function Timer({ startAt }: Props) {
let rAF = requestAnimationFrame(loop);
return () => cancelAnimationFrame(rAF);
}, [startAt]);
}, [override, startAt]);
return (
<div>

View File

@@ -18,7 +18,7 @@ import { WorkflowRun } from "./routes/workflows/WorkflowRun";
import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters";
import { Workflows } from "./routes/workflows/Workflows";
import { WorkflowsPageLayout } from "./routes/workflows/WorkflowsPageLayout";
import { WorkflowDebugger } from "./routes/workflows/editor/WorkflowDebugger";
import { Debugger } from "./routes/workflows/debugger/Debugger";
import { WorkflowEditor } from "./routes/workflows/editor/WorkflowEditor";
import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/WorkflowPostRunParameters";
import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput";
@@ -111,11 +111,11 @@ const router = createBrowserRouter([
},
{
path: "debug",
element: <WorkflowDebugger />,
element: <Debugger />,
},
{
path: ":workflowRunId/:blockLabel/debug",
element: <WorkflowDebugger />,
element: <Debugger />,
},
{
path: "edit",

View File

@@ -4,12 +4,12 @@ import { ReactFlowProvider } from "@xyflow/react";
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
import { WorkflowSettings } from "../types/workflowTypes";
import { getElements } from "./workflowEditorUtils";
import { getInitialParameters } from "./utils";
import { Workspace } from "./Workspace";
import { getElements } from "@/routes/workflows/editor/workflowEditorUtils";
import { getInitialParameters } from "@/routes/workflows/editor/utils";
import { Workspace } from "@/routes/workflows/editor/Workspace";
import { useWorkflowParametersStore } from "@/store/WorkflowParametersStore";
function WorkflowDebugger() {
function Debugger() {
const { workflowPermanentId } = useParams();
const { data: workflow } = useWorkflowQuery({
workflowPermanentId,
@@ -64,4 +64,4 @@ function WorkflowDebugger() {
);
}
export { WorkflowDebugger };
export { Debugger };

View File

@@ -0,0 +1,201 @@
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
import { CodeEditor } from "../components/CodeEditor";
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
import { useActiveWorkflowRunItem } from "@/routes/workflows/workflowRun/useActiveWorkflowRunItem";
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
import { isAction, isWorkflowRunBlock } from "../types/workflowRunTypes";
import { findBlockSurroundingAction } from "@/routes/workflows/workflowRun/workflowTimelineUtils";
import { DebuggerTaskBlockParameters } from "./DebuggerTaskBlockParameters";
import { isTaskVariantBlock, WorkflowBlockTypes } from "../types/workflowTypes";
import { Input } from "@/components/ui/input";
import { ProxySelector } from "@/components/ProxySelector";
import { DebuggerSendEmailBlockParameters } from "./DebuggerSendEmailBlockInfo";
import { ProxyLocation } from "@/api/types";
import { KeyValueInput } from "@/components/KeyValueInput";
import { HelpTooltip } from "@/components/HelpTooltip";
function DebuggerPostRunParameters() {
const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } =
useWorkflowRunTimelineQuery();
const [activeItem] = useActiveWorkflowRunItem();
const { data: workflowRun, isLoading: workflowRunIsLoading } =
useWorkflowRunQuery();
const parameters = workflowRun?.parameters ?? {};
if (workflowRunIsLoading || workflowRunTimelineIsLoading) {
return <div>Loading workflow parameters...</div>;
}
if (!workflowRun || !workflowRunTimeline) {
return null;
}
function getActiveBlock() {
if (!workflowRunTimeline) {
return;
}
if (isWorkflowRunBlock(activeItem)) {
return activeItem;
}
if (isAction(activeItem)) {
return findBlockSurroundingAction(
workflowRunTimeline,
activeItem.action_id,
);
}
}
const activeBlock = getActiveBlock();
const isTaskV2 = workflowRun.task_v2 !== null;
const webhookCallbackUrl = isTaskV2
? workflowRun.task_v2?.webhook_callback_url
: workflowRun.webhook_callback_url;
const proxyLocation = isTaskV2
? workflowRun.task_v2?.proxy_location
: workflowRun.proxy_location;
const extraHttpHeaders = isTaskV2
? workflowRun.task_v2?.extra_http_headers
: workflowRun.extra_http_headers;
return (
<div className="space-y-5">
{activeBlock && isTaskVariantBlock(activeBlock) ? (
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Task Block Parameters</h1>
<DebuggerTaskBlockParameters block={activeBlock} />
</div>
</div>
) : null}
{activeBlock &&
activeBlock.block_type === WorkflowBlockTypes.SendEmail ? (
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Email Block Parameters</h1>
<DebuggerSendEmailBlockParameters
body={activeBlock?.body ?? ""}
recipients={activeBlock?.recipients ?? []}
subject={activeBlock?.subject ?? ""}
/>
</div>
</div>
) : null}
{activeBlock && activeBlock.block_type === WorkflowBlockTypes.ForLoop ? (
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">For Loop Block Parameters</h1>
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Loop Values</h1>
<HelpTooltip content="The values that are being looped over." />
</div>
<CodeEditor
className="w-full"
language="json"
value={JSON.stringify(activeBlock?.loop_values, null, 2)}
readOnly
minHeight="96px"
maxHeight="200px"
/>
</div>
</div>
</div>
) : null}
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Workflow Parameters</h1>
{Object.entries(parameters).map(([key, value]) => {
return (
<div key={key} className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">{key}</h1>
<HelpTooltip content="The value of the parameter." />
</div>
{typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ? (
<AutoResizingTextarea value={String(value)} readOnly />
) : (
<CodeEditor
value={JSON.stringify(value, null, 2)}
readOnly
language="json"
minHeight="96px"
maxHeight="200px"
className="w-full"
/>
)}
</div>
);
})}
{Object.entries(parameters).length === 0 ? (
<div className="text-sm">
No input parameters found for this workflow
</div>
) : null}
<h1 className="text-sm font-bold">Other Workflow Parameters</h1>
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Webhook Callback URL</h1>
<HelpTooltip content="The webhook callback URL for the workflow." />
</div>
<Input value={webhookCallbackUrl ?? ""} readOnly />
</div>
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Proxy Location</h1>
<HelpTooltip content="The proxy location for the workflow." />
</div>
<ProxySelector
value={proxyLocation ?? ProxyLocation.Residential}
onChange={() => {
// TODO
}}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Extra HTTP Headers</h1>
<HelpTooltip content="The extra HTTP headers for the workflow." />
</div>
<div className="w-full">
<KeyValueInput
value={
extraHttpHeaders ? JSON.stringify(extraHttpHeaders) : null
}
readOnly={true}
onChange={() => {}}
/>
</div>
</div>
</div>
</div>
{workflowRun.task_v2 ? (
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Task 2.0 Parameters</h1>
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Task 2.0 Prompt</h1>
<HelpTooltip content="The original prompt for the task." />
</div>
<AutoResizingTextarea
value={workflowRun.task_v2?.prompt ?? ""}
readOnly
/>
</div>
<AutoResizingTextarea
value={workflowRun.task_v2?.prompt ?? ""}
readOnly
/>
</div>
</div>
) : null}
</div>
);
}
export { DebuggerPostRunParameters };

View File

@@ -1,12 +1,12 @@
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { WorkflowDebuggerRunTimeline } from "./WorkflowDebuggerRunTimeline";
import { DebuggerRunTimeline } from "./DebuggerRunTimeline";
function WorkflowDebuggerRun() {
function DebuggerRun() {
const { data: workflowRun } = useWorkflowRunQuery();
const workflowFailureReason = workflowRun?.failure_reason ? (
<div
className="m-4 w-full rounded-md border border-red-600 p-4"
className="align-self-start max-h-[8rem] w-full overflow-y-auto rounded-md border border-red-600 p-4"
style={{
backgroundColor: "rgba(220, 38, 38, 0.10)",
width: "calc(100% - 2rem)",
@@ -19,19 +19,17 @@ function WorkflowDebuggerRun() {
return (
<div className="flex h-full w-full flex-col items-center justify-start overflow-hidden overflow-y-auto">
<div className="flex h-full w-full flex-col items-center justify-start gap-4 bg-[#0c1121]">
{workflowFailureReason}
<div className="h-full w-full">
<WorkflowDebuggerRunTimeline
activeItem="stream"
onActionItemSelected={() => {}}
onBlockItemSelected={() => {}}
onObserverThoughtCardSelected={() => {}}
/>
</div>
{workflowFailureReason}
<div className="h-full w-full">
<DebuggerRunTimeline
activeItem="stream"
onActionItemSelected={() => {}}
onBlockItemSelected={() => {}}
onObserverThoughtCardSelected={() => {}}
/>
</div>
</div>
);
}
export { WorkflowDebuggerRun };
export { DebuggerRun };

View File

@@ -0,0 +1,175 @@
import { FileIcon } from "@radix-ui/react-icons";
import { CodeEditor } from "../components/CodeEditor";
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
import { useActiveWorkflowRunItem } from "@/routes/workflows/workflowRun/useActiveWorkflowRunItem";
import {
hasExtractedInformation,
isAction,
isWorkflowRunBlock,
} from "../types/workflowRunTypes";
import { findBlockSurroundingAction } from "@/routes/workflows/workflowRun/workflowTimelineUtils";
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
import { Status } from "@/api/types";
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
import { isTaskVariantBlock } from "../types/workflowTypes";
function DebuggerRunOutput() {
const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } =
useWorkflowRunTimelineQuery();
const [activeItem] = useActiveWorkflowRunItem();
const { data: workflowRun } = useWorkflowRunQuery();
if (workflowRunTimelineIsLoading) {
return <div>Loading...</div>;
}
if (!workflowRunTimeline) {
return null;
}
function getActiveBlock() {
if (!workflowRunTimeline) {
return;
}
if (isWorkflowRunBlock(activeItem)) {
return activeItem;
}
if (isAction(activeItem)) {
return findBlockSurroundingAction(
workflowRunTimeline,
activeItem.action_id,
);
}
}
const activeBlock = getActiveBlock();
const showExtractedInformation =
activeBlock &&
isTaskVariantBlock(activeBlock) &&
activeBlock.status === Status.Completed;
const outputs = workflowRun?.outputs;
const fileUrls = workflowRun?.downloaded_file_urls ?? [];
const observerOutput = workflowRun?.task_v2?.output;
const webhookFailureReasonData =
workflowRun?.task_v2?.webhook_failure_reason ??
workflowRun?.webhook_failure_reason;
return (
<div className="space-y-5">
{webhookFailureReasonData ? (
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Webhook Failure Reason</h1>
<div className="space-y-2 text-yellow-600">
{webhookFailureReasonData}
</div>
</div>
</div>
) : null}
{activeBlock ? (
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Block Outputs</h1>
{activeBlock.output === null ? (
<div className="text-sm">This block has no outputs</div>
) : isTaskVariantBlock(activeBlock) ? (
<div className="space-y-2">
<h2 className="text-sm">
{showExtractedInformation
? "Extracted Information"
: "Failure Reason"}
</h2>
{showExtractedInformation ? (
<CodeEditor
language="json"
value={JSON.stringify(
(hasExtractedInformation(activeBlock.output) &&
activeBlock.output.extracted_information) ??
null,
null,
2,
)}
minHeight="96px"
maxHeight="200px"
readOnly
/>
) : (
<AutoResizingTextarea
value={
activeBlock.status === "canceled"
? "This block was cancelled"
: activeBlock.failure_reason ?? ""
}
readOnly
/>
)}
</div>
) : (
<div className="space-y-2">
<h2 className="text-sm">Output</h2>
<CodeEditor
language="json"
value={JSON.stringify(activeBlock.output, null, 2)}
minHeight="96px"
maxHeight="200px"
readOnly
/>
</div>
)}
</div>
</div>
) : null}
{observerOutput ? (
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Task 2.0 Output</h1>
<CodeEditor
language="json"
value={JSON.stringify(observerOutput, null, 2)}
readOnly
minHeight="96px"
maxHeight="200px"
/>
</div>
</div>
) : null}
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Workflow Run Outputs</h1>
<CodeEditor
language="json"
value={outputs ? JSON.stringify(outputs, null, 2) : ""}
readOnly
minHeight="96px"
maxHeight="200px"
/>
</div>
</div>
<div className="rounded bg-slate-elevation2 p-6">
<div className="space-y-4">
<h1 className="text-sm font-bold">Workflow Run Downloaded Files</h1>
<div className="space-y-2">
{fileUrls.length > 0 ? (
fileUrls.map((url, index) => {
return (
<div key={url} title={url} className="flex gap-2">
<FileIcon className="size-6" />
<a href={url} className="underline underline-offset-4">
<span>{`File ${index + 1}`}</span>
</a>
</div>
);
})
) : (
<div className="text-sm">No files downloaded</div>
)}
</div>
</div>
</div>
</div>
);
}
export { DebuggerRunOutput };

View File

@@ -25,7 +25,7 @@ type Props = {
onBlockItemSelected: (item: WorkflowRunBlock) => void;
};
function WorkflowDebuggerRunTimeline({
function DebuggerRunTimeline({
activeItem,
onObserverThoughtCardSelected,
onActionItemSelected,
@@ -126,4 +126,4 @@ function WorkflowDebuggerRunTimeline({
);
}
export { WorkflowDebuggerRunTimeline };
export { DebuggerRunTimeline };

View File

@@ -0,0 +1,43 @@
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
import { Input } from "@/components/ui/input";
import { HelpTooltip } from "@/components/HelpTooltip";
type Props = {
recipients: Array<string>;
body: string;
subject: string;
};
function DebuggerSendEmailBlockParameters({
recipients,
body,
subject,
}: Props) {
return (
<div className="space-y-4">
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">To</h1>
<HelpTooltip content="The recipients of the email." />
</div>
<Input value={recipients.join(", ")} readOnly />
</div>
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Subject</h1>
<HelpTooltip content="The subject of the email." />
</div>
<Input value={subject} readOnly />
</div>
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Body</h1>
<HelpTooltip content="The body of the email." />
</div>
<AutoResizingTextarea value={body} readOnly />
</div>
</div>
);
}
export { DebuggerSendEmailBlockParameters };

View File

@@ -0,0 +1,147 @@
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
import { Input } from "@/components/ui/input";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { WorkflowRunBlock } from "../types/workflowRunTypes";
import { isTaskVariantBlock, WorkflowBlockTypes } from "../types/workflowTypes";
import { Switch } from "@/components/ui/switch";
import { HelpTooltip } from "@/components/HelpTooltip";
type Props = {
block: WorkflowRunBlock;
};
function DebuggerTaskBlockParameters({ block }: Props) {
const isTaskVariant = isTaskVariantBlock(block);
if (!isTaskVariant) {
return null;
}
const showNavigationParameters =
block.block_type === WorkflowBlockTypes.Task ||
block.block_type === WorkflowBlockTypes.Action ||
block.block_type === WorkflowBlockTypes.Login ||
block.block_type === WorkflowBlockTypes.Navigation;
const showDataExtractionParameters =
block.block_type === WorkflowBlockTypes.Task ||
block.block_type === WorkflowBlockTypes.Extraction;
const showValidationParameters =
block.block_type === WorkflowBlockTypes.Validation;
const showIncludeActionHistoryInVerification =
block.block_type === WorkflowBlockTypes.Task ||
block.block_type === WorkflowBlockTypes.Navigation;
return (
<>
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">URL</h1>
<HelpTooltip content="The starting URL for the block." />
</div>
<Input value={block.url ?? ""} readOnly />
</div>
{showNavigationParameters ? (
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Navigation Goal</h1>
<HelpTooltip content="What should Skyvern do on this page?" />
</div>
<AutoResizingTextarea value={block.navigation_goal ?? ""} readOnly />
</div>
) : null}
{showNavigationParameters ? (
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-nowrap text-sm">Navigation Payload</h1>
<HelpTooltip content="Specify important parameters, routes, or states." />
</div>
<CodeEditor
className="w-full"
language="json"
value={JSON.stringify(block.navigation_payload, null, 2)}
readOnly
minHeight="96px"
maxHeight="200px"
/>
</div>
) : null}
{showDataExtractionParameters ? (
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Data Extraction Goal</h1>
<HelpTooltip content="What outputs are you looking to get?" />
</div>
<AutoResizingTextarea
value={block.data_extraction_goal ?? ""}
readOnly
/>
</div>
) : null}
{showDataExtractionParameters ? (
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Data Schema</h1>
<HelpTooltip content="Specify the output format in JSON" />
</div>
<CodeEditor
className="w-full"
language="json"
value={JSON.stringify(block.data_schema, null, 2)}
readOnly
minHeight="96px"
maxHeight="200px"
/>
</div>
) : null}
{showValidationParameters ? (
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Completion Criteria</h1>
<HelpTooltip content="Complete if..." />
</div>
<AutoResizingTextarea
value={block.complete_criterion ?? ""}
readOnly
/>
</div>
) : null}
{showValidationParameters ? (
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-sm">Termination Criteria</h1>
<HelpTooltip content="Terminate if..." />
</div>
<AutoResizingTextarea
value={block.terminate_criterion ?? ""}
readOnly
/>
</div>
) : null}
{showIncludeActionHistoryInVerification ? (
<div className="flex flex-col gap-2">
<div className="flex w-full items-center justify-start gap-2">
<h1 className="text-nowrap text-sm">Include Action History</h1>
<HelpTooltip content="Whether to include action history in the completion verification" />
</div>
<div className="w-full">
<Switch
checked={block.include_action_history_in_verification ?? false}
disabled
/>
</div>
</div>
) : null}
</>
);
}
export { DebuggerTaskBlockParameters };

View File

@@ -226,7 +226,7 @@ type Props = {
initialTitle: string;
// initialParameters: ParametersState;
workflow: WorkflowApiResponse;
onDebuggableBlockCountChange: (count: number) => void;
onDebuggableBlockCountChange?: (count: number) => void;
onMouseDownCapture?: () => void;
zIndex?: number;
};
@@ -305,7 +305,7 @@ function FlowRenderer({
}
}
onDebuggableBlockCountChange(debuggable.length);
onDebuggableBlockCountChange?.(debuggable.length);
}, [nodes, edges, onDebuggableBlockCountChange]);
const constructSaveData = useCallback((): WorkflowSaveData => {

View File

@@ -15,17 +15,18 @@ import {
ReloadIcon,
} from "@radix-ui/react-icons";
import { useNavigate, useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
import { EditableNodeTitle } from "./nodes/components/EditableNodeTitle";
import { useCreateWorkflowMutation } from "../hooks/useCreateWorkflowMutation";
import { convert } from "./workflowEditorUtils";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { useDebugStore } from "@/store/useDebugStore";
import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
import { cn } from "@/util/utils";
type Props = {
debuggableBlockCount: number;
parametersPanelOpen: boolean;
onParametersClick: () => void;
onSave: () => void;
@@ -34,7 +35,6 @@ type Props = {
};
function WorkflowHeader({
debuggableBlockCount,
parametersPanelOpen,
onParametersClick,
onSave,
@@ -43,13 +43,14 @@ function WorkflowHeader({
}: Props) {
const { title, setTitle } = useWorkflowTitleStore();
const workflowChangesStore = useWorkflowHasChangesStore();
const { blockLabel: urlBlockLabel, workflowPermanentId } = useParams();
const { workflowPermanentId } = useParams();
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
const navigate = useNavigate();
const createWorkflowMutation = useCreateWorkflowMutation();
const { data: workflowRun } = useWorkflowRunQuery();
const debugStore = useDebugStore();
const anyBlockIsPlaying =
urlBlockLabel !== undefined && urlBlockLabel.length > 0;
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
if (!globalWorkflows) {
return null; // this should be loaded already by some other components
@@ -105,7 +106,7 @@ function WorkflowHeader({
<Button
size="lg"
variant={debugStore.isDebugMode ? "default" : "tertiary"}
disabled={debuggableBlockCount === 0 || anyBlockIsPlaying}
disabled={workflowRunIsRunningOrQueued}
onClick={() => {
if (debugStore.isDebugMode) {
navigate(`/workflows/${workflowPermanentId}/edit`);

View File

@@ -27,12 +27,15 @@ import {
DialogTitle,
DialogClose,
} from "@/components/ui/dialog";
import { SwitchBar } from "@/components/SwitchBar";
import { toast } from "@/components/ui/use-toast";
import { BrowserStream } from "@/components/BrowserStream";
import { FloatingWindow } from "@/components/FloatingWindow";
import { statusIsFinalized } from "@/routes/tasks/types.ts";
import { WorkflowDebuggerRun } from "@/routes/workflows/editor/WorkflowDebuggerRun";
import { DebuggerRun } from "@/routes/workflows/debugger/DebuggerRun";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
import { DebuggerRunOutput } from "@/routes/workflows/debugger/DebuggerRunOutput";
import { DebuggerPostRunParameters } from "@/routes/workflows/debugger/DebuggerPostRunParameters";
import { useDebugStore } from "@/store/useDebugStore";
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
import {
@@ -80,11 +83,11 @@ function Workspace({
showBrowser = false,
workflow,
}: Props) {
const { blockLabel, workflowPermanentId } = useParams();
const { blockLabel, workflowPermanentId, workflowRunId } = useParams();
const [content, setContent] = useState("actions");
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
useWorkflowPanelStore();
const debugStore = useDebugStore();
const [debuggableBlockCount, setDebuggableBlockCount] = useState(0);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const saveWorkflow = useWorkflowSave();
@@ -384,7 +387,6 @@ function Workspace({
}}
>
<WorkflowHeader
debuggableBlockCount={debuggableBlockCount}
saving={workflowChangesStore.saveIsPending}
parametersPanelOpen={
workflowPanelState.active &&
@@ -468,9 +470,38 @@ function Workspace({
promote("history");
}}
>
<div className="pointer-events-none absolute right-0 top-0 flex h-full w-[400px] flex-col items-end justify-end">
<div className="pointer-events-auto relative h-full w-full overflow-hidden rounded-xl border-2 border-slate-500">
<WorkflowDebuggerRun />
<div className="pointer-events-none absolute right-0 top-0 flex h-full w-[400px] flex-col items-end justify-end bg-slate-900">
<div className="pointer-events-auto relative flex h-full w-full flex-col items-start overflow-hidden rounded-xl border-2 border-slate-500">
{workflowRunId && (
<SwitchBar
className="m-2 border-none"
onChange={(value) => setContent(value)}
value={content}
options={[
{
label: "Actions",
value: "actions",
},
{
label: "Inputs",
value: "inputs",
},
{
label: "Outputs",
value: "outputs",
},
]}
/>
)}
<div className="h-full w-full overflow-hidden overflow-y-auto">
{(!workflowRunId || content === "actions") && <DebuggerRun />}
{workflowRunId && content === "inputs" && (
<DebuggerPostRunParameters />
)}
{workflowRunId && content === "outputs" && (
<DebuggerRunOutput />
)}
</div>
</div>
</div>
</div>
@@ -487,7 +518,6 @@ function Workspace({
initialTitle={initialTitle}
// initialParameters={initialParameters}
workflow={workflow}
onDebuggableBlockCountChange={(c) => setDebuggableBlockCount(c)}
onMouseDownCapture={() => promote("infiniteCanvas")}
zIndex={rankedItems.infiniteCanvas}
/>

View File

@@ -38,6 +38,8 @@ import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { cn } from "@/util/utils";
import { useParams } from "react-router-dom";
import { NodeHeader } from "../components/NodeHeader";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
const urlTooltip =
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
@@ -67,8 +69,13 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
});
const { blockLabel: urlBlockLabel } = useParams();
const debugStore = useDebugStore();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const nodes = useNodes<AppNode>();
@@ -108,8 +115,9 @@ function ActionNode({ id, data, type }: NodeProps<ActionNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -8,6 +8,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
const { updateNodeData } = useReactFlow();
@@ -15,8 +17,13 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
code: data.code,
parameterKeys: data.parameterKeys,
@@ -40,8 +47,9 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -8,14 +8,21 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
const { debuggable, editable, label } = data;
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
return (
<div>
@@ -35,8 +42,9 @@ function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -38,6 +38,8 @@ import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
const { updateNodeData } = useReactFlow();
@@ -48,8 +50,13 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
url: data.url,
dataExtractionGoal: data.dataExtractionGoal,
@@ -97,8 +104,9 @@ function ExtractionNode({ id, data, type }: NodeProps<ExtractionNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -38,6 +38,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
const urlTooltip =
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
@@ -54,8 +56,13 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
const script = blockScriptStore.scripts[label];
const debugStore = useDebugStore();
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const [inputs, setInputs] = useState({
url: data.url,
@@ -105,8 +112,9 @@ function FileDownloadNode({ id, data }: NodeProps<FileDownloadNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -12,6 +12,8 @@ import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { WorkflowDataSchemaInputGroup } from "@/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup";
import { dataSchemaExampleForFileExtraction } from "../types";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
const { updateNodeData } = useReactFlow();
@@ -19,8 +21,13 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
fileUrl: data.fileUrl,
jsonSchema: data.jsonSchema,
@@ -54,8 +61,9 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -17,6 +17,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
const { updateNodeData } = useReactFlow();
@@ -24,8 +26,13 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
storageType: data.storageType,
@@ -65,8 +72,9 @@ function FileUploadNode({ id, data }: NodeProps<FileUploadNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -39,6 +39,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
const { updateNodeData } = useReactFlow();
@@ -49,8 +51,13 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
url: data.url,
navigationGoal: data.navigationGoal,
@@ -102,8 +109,9 @@ function LoginNode({ id, data, type }: NodeProps<LoginNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -20,6 +20,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function LoopNode({ id, data }: NodeProps<LoopNode>) {
const { updateNodeData } = useReactFlow();
@@ -32,8 +34,13 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
loopVariableReference: data.loopVariableReference,
});
@@ -94,8 +101,9 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -39,6 +39,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { useParams } from "react-router-dom";
import { NodeHeader } from "../components/NodeHeader";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
const { blockLabel: urlBlockLabel } = useParams();
@@ -48,8 +50,13 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
const blockScriptStore = useBlockScriptStore();
const { editable, debuggable, label } = data;
const script = blockScriptStore.scripts[label];
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const [inputs, setInputs] = useState({
allowDownloads: data.allowDownloads,
@@ -107,8 +114,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -12,6 +12,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
const { updateNodeData } = useReactFlow();
@@ -19,8 +21,13 @@ function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
fileUrl: data.fileUrl,
jsonSchema: data.jsonSchema,
@@ -54,8 +61,9 @@ function PDFParserNode({ id, data }: NodeProps<PDFParserNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -12,6 +12,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
const { updateNodeData } = useReactFlow();
@@ -19,8 +21,13 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
recipients: data.recipients,
subject: data.subject,
@@ -56,8 +63,9 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -40,6 +40,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
const { updateNodeData } = useReactFlow();
@@ -50,8 +52,13 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const nodes = useNodes<AppNode>();
const edges = useEdges();
const outputParameterKeys = getAvailableOutputParameterKeys(nodes, edges, id);
@@ -108,8 +115,9 @@ function TaskNode({ id, data, type }: NodeProps<TaskNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -19,14 +19,21 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
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 =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const { updateNodeData } = useReactFlow();
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
@@ -65,8 +72,9 @@ function Taskv2Node({ id, data, type }: NodeProps<Taskv2Node>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -14,6 +14,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
const { updateNodeData } = useReactFlow();
@@ -21,8 +23,13 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
prompt: data.prompt,
jsonSchema: data.jsonSchema,
@@ -57,8 +64,9 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -13,6 +13,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function URLNode({ id, data, type }: NodeProps<URLNode>) {
const { updateNodeData } = useReactFlow();
@@ -23,8 +25,13 @@ function URLNode({ id, data, type }: NodeProps<URLNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id });
const [inputs, setInputs] = useState({
@@ -62,8 +69,9 @@ function URLNode({ id, data, type }: NodeProps<URLNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -8,14 +8,20 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function UploadNode({ id, data }: NodeProps<UploadNode>) {
const { debuggable, editable, label } = data;
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
return (
<div>
@@ -35,8 +41,9 @@ function UploadNode({ id, data }: NodeProps<UploadNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -36,6 +36,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
const { updateNodeData } = useReactFlow();
@@ -46,8 +48,13 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
completeCriterion: data.completeCriterion,
terminateCriterion: data.terminateCriterion,
@@ -91,8 +98,9 @@ function ValidationNode({ id, data, type }: NodeProps<ValidationNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -10,6 +10,8 @@ import { useDebugStore } from "@/store/useDebugStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
function WaitNode({ id, data, type }: NodeProps<WaitNode>) {
const { updateNodeData } = useReactFlow();
@@ -17,8 +19,13 @@ function WaitNode({ id, data, type }: NodeProps<WaitNode>) {
const debugStore = useDebugStore();
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
const { blockLabel: urlBlockLabel } = useParams();
const thisBlockIsPlaying =
const { data: workflowRun } = useWorkflowRunQuery();
const workflowRunIsRunningOrQueued =
workflowRun && statusIsRunningOrQueued(workflowRun);
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === label;
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued && thisBlockIsTargetted;
const [inputs, setInputs] = useState({
waitInSeconds: data.waitInSeconds,
});
@@ -51,8 +58,9 @@ function WaitNode({ id, data, type }: NodeProps<WaitNode>) {
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,
"pointer-events-none": thisBlockIsPlaying,
"bg-slate-950 outline outline-2 outline-slate-300":
thisBlockIsTargetted,
},
)}
>

View File

@@ -138,10 +138,6 @@ function NodeHeader({
} = useParams();
const debugStore = useDebugStore();
const { closeWorkflowPanel } = useWorkflowPanelStore();
const thisBlockIsPlaying =
urlBlockLabel !== undefined && urlBlockLabel === blockLabel;
const anyBlockIsPlaying =
urlBlockLabel !== undefined && urlBlockLabel.length > 0;
const workflowSettingsStore = useWorkflowSettingsStore();
const [label, setLabel] = useNodeLabelChangeHandler({
id: nodeId,
@@ -164,6 +160,21 @@ function NodeHeader({
});
const saveWorkflow = useWorkflowSave();
const thisBlockIsPlaying =
workflowRunIsRunningOrQueued &&
urlBlockLabel !== undefined &&
urlBlockLabel === blockLabel;
const thisBlockIsTargetted =
urlBlockLabel !== undefined && urlBlockLabel === blockLabel;
const timerDurationOverride =
workflowRun && workflowRun.finished_at
? new Date(workflowRun.finished_at).getTime() -
new Date(workflowRun.created_at).getTime() +
3500
: null;
useEffect(() => {
if (!workflowRun || !workflowPermanentId || !workflowRunId) {
return;
@@ -173,7 +184,7 @@ function NodeHeader({
workflowRunId === workflowRun?.workflow_run_id &&
statusIsFinalized(workflowRun)
) {
navigate(`/workflows/${workflowPermanentId}/debug`);
// navigate(`/workflows/${workflowPermanentId}/debug`);
if (statusIsAFailureType(workflowRun)) {
toast({
@@ -340,10 +351,10 @@ function NodeHeader({
return (
<>
{thisBlockIsPlaying && (
{thisBlockIsTargetted && (
<div className="flex w-full animate-[auto-height_1s_ease-in-out_forwards] items-center justify-between overflow-hidden">
<div className="pb-4">
<Timer />
<Timer override={timerDurationOverride ?? undefined} />
</div>
<div className="pb-4">{workflowRun?.status ?? "pending"}</div>
</div>
@@ -370,7 +381,7 @@ function NodeHeader({
</div>
</div>
<div className="pointer-events-auto ml-auto flex items-center gap-2">
{thisBlockIsPlaying && workflowRunIsRunningOrQueued && (
{thisBlockIsPlaying && (
<div className="ml-auto">
<button className="rounded p-1 hover:bg-red-500 hover:text-black disabled:opacity-50">
{cancelBlock.isPending ? (
@@ -388,9 +399,9 @@ function NodeHeader({
)}
{debugStore.isDebugMode && isDebuggable && (
<button
disabled={anyBlockIsPlaying}
disabled={workflowRunIsRunningOrQueued}
className={cn("rounded p-1 disabled:opacity-50", {
"hover:bg-muted": anyBlockIsPlaying,
"hover:bg-muted": workflowRunIsRunningOrQueued,
})}
>
{runBlock.isPending ? (
@@ -399,7 +410,7 @@ function NodeHeader({
<PlayIcon
className={cn("size-6", {
"fill-gray-500 text-gray-500":
anyBlockIsPlaying || !workflowPermanentId,
workflowRunIsRunningOrQueued || !workflowPermanentId,
})}
onClick={() => {
handleOnPlay();
@@ -412,7 +423,8 @@ function NodeHeader({
<div>
<div
className={cn("rounded p-1 hover:bg-muted", {
"pointer-events-none opacity-50": anyBlockIsPlaying,
"pointer-events-none opacity-50":
workflowRunIsRunningOrQueued,
})}
>
<NodeActionMenu