Debugger Lite (#2970)
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import { Status } from "@/api/types";
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
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";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
@@ -29,6 +27,7 @@ type Command = CommandTakeControl | CommandCedeControl;
|
||||
|
||||
type Props = {
|
||||
browserSessionId?: string;
|
||||
interactive?: boolean;
|
||||
task?: {
|
||||
run: TaskApiResponse;
|
||||
};
|
||||
@@ -41,6 +40,7 @@ type Props = {
|
||||
|
||||
function BrowserStream({
|
||||
browserSessionId = undefined,
|
||||
interactive = true,
|
||||
task = undefined,
|
||||
workflow = undefined,
|
||||
// --
|
||||
@@ -67,7 +67,6 @@ function BrowserStream({
|
||||
}
|
||||
|
||||
const [commandSocket, setCommandSocket] = useState<WebSocket | null>(null);
|
||||
const [userIsControlling, setUserIsControlling] = useState<boolean>(false);
|
||||
const [vncDisconnectedTrigger, setVncDisconnectedTrigger] = useState(0);
|
||||
const prevVncConnectedRef = useRef<boolean>(false);
|
||||
const [isVncConnected, setIsVncConnected] = useState<boolean>(false);
|
||||
@@ -273,13 +272,13 @@ function BrowserStream({
|
||||
commandSocket.send(JSON.stringify(command));
|
||||
};
|
||||
|
||||
if (userIsControlling) {
|
||||
if (interactive) {
|
||||
sendCommand({ kind: "take-control" });
|
||||
} else {
|
||||
sendCommand({ kind: "cede-control" });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userIsControlling, isCommandConnected]);
|
||||
}, [interactive, isCommandConnected]);
|
||||
|
||||
// Effect to show toast when task or workflow reaches a final state based on hook updates
|
||||
useEffect(() => {
|
||||
@@ -316,39 +315,11 @@ function BrowserStream({
|
||||
return (
|
||||
<div
|
||||
className={cn("browser-stream", {
|
||||
"user-is-controlling": userIsControlling,
|
||||
"user-is-controlling": interactive,
|
||||
})}
|
||||
ref={setCanvasContainerRef}
|
||||
>
|
||||
{isVncConnected && (
|
||||
<div className="overlay-container">
|
||||
<div className="overlay">
|
||||
<Button
|
||||
className={cn(
|
||||
"take-control absolute bottom-[-1rem] left-[1rem]",
|
||||
{ hide: userIsControlling },
|
||||
)}
|
||||
type="button"
|
||||
onClick={() => setUserIsControlling(true)}
|
||||
>
|
||||
<HandIcon className="mr-2 h-4 w-4" />
|
||||
interact
|
||||
</Button>
|
||||
<div className="absolute bottom-[-1rem] right-[1rem]">
|
||||
<Button
|
||||
className={cn("relinquish-control", {
|
||||
hide: !userIsControlling,
|
||||
})}
|
||||
type="button"
|
||||
onClick={() => setUserIsControlling(false)}
|
||||
>
|
||||
<StopIcon className="mr-2 h-4 w-4" />
|
||||
stop interacting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isVncConnected && <div className="overlay" />}
|
||||
{!isVncConnected && (
|
||||
<div className="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-black">
|
||||
<Skeleton className="aspect-[16/9] h-auto max-h-full w-full max-w-full rounded-lg object-cover" />
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* and `re-resizable`; but I don't want to do that until it's worth the effort.)
|
||||
*/
|
||||
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { Resizable } from "re-resizable";
|
||||
import {
|
||||
useCallback,
|
||||
@@ -69,6 +70,14 @@ function WindowsButton(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function ReloadButton(props: { isReloading: boolean; onClick: () => void }) {
|
||||
return (
|
||||
<button onClick={() => props.onClick()}>
|
||||
<ReloadIcon className={props.isReloading ? "animate-spin" : undefined} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function getOs(): OS {
|
||||
if (typeof navigator === "undefined") {
|
||||
return "Unknown"; // For non-browser environments
|
||||
@@ -105,6 +114,7 @@ function FloatingWindow({
|
||||
showCloseButton,
|
||||
showMaximizeButton,
|
||||
showMinimizeButton,
|
||||
showReloadButton = false,
|
||||
title,
|
||||
zIndex,
|
||||
// --
|
||||
@@ -118,11 +128,14 @@ function FloatingWindow({
|
||||
showCloseButton?: boolean;
|
||||
showMaximizeButton?: boolean;
|
||||
showMinimizeButton?: boolean;
|
||||
showReloadButton?: boolean;
|
||||
title: string;
|
||||
zIndex?: string;
|
||||
// --
|
||||
onInteract?: () => void;
|
||||
}) {
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const [isReloading, setIsReloading] = useState(false);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [size, setSize] = useState({
|
||||
left: 0,
|
||||
@@ -394,6 +407,19 @@ function FloatingWindow({
|
||||
setIsMinimized(false);
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
if (isReloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReloadKey((prev) => prev + 1);
|
||||
setIsReloading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsReloading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* If maximized, need to retain max size during parent resizing.
|
||||
*/
|
||||
@@ -507,6 +533,7 @@ function FloatingWindow({
|
||||
>
|
||||
<div
|
||||
ref={resizableRef}
|
||||
key={reloadKey}
|
||||
className="my-window"
|
||||
style={{
|
||||
pointerEvents: "auto",
|
||||
@@ -523,7 +550,7 @@ function FloatingWindow({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"my-window-header flex h-[3rem] w-full cursor-move items-center justify-start gap-2 bg-[#031827] p-3",
|
||||
"my-window-header flex h-[3rem] w-full cursor-move items-center justify-start gap-2 bg-[#131519] p-3",
|
||||
)}
|
||||
>
|
||||
{os === "macOS" ? (
|
||||
@@ -557,9 +584,21 @@ function FloatingWindow({
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto">{title}</div>
|
||||
{showReloadButton && (
|
||||
<ReloadButton
|
||||
isReloading={isReloading}
|
||||
onClick={() => reload()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showReloadButton && (
|
||||
<ReloadButton
|
||||
isReloading={isReloading}
|
||||
onClick={() => reload()}
|
||||
/>
|
||||
)}
|
||||
<div>{title}</div>
|
||||
<div className="buttons-container ml-auto flex h-full items-center gap-2">
|
||||
{showMinimizeButton && (
|
||||
|
||||
@@ -13,35 +13,18 @@
|
||||
padding: 0rem;
|
||||
}
|
||||
|
||||
.browser-stream .overlay-container {
|
||||
.browser-stream .overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.browser-stream .overlay {
|
||||
position: relative;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
object-fit: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.browser-stream.user-is-controlling .overlay {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.browser-stream.user-is-controlling .overlay-container {
|
||||
pointer-events: none;
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
.browser-stream .take-control {
|
||||
|
||||
@@ -18,6 +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 { WorkflowEditor } from "./routes/workflows/editor/WorkflowEditor";
|
||||
import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/WorkflowPostRunParameters";
|
||||
import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput";
|
||||
@@ -110,11 +111,11 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "debug",
|
||||
element: <WorkflowEditor />,
|
||||
element: <WorkflowDebugger />,
|
||||
},
|
||||
{
|
||||
path: ":workflowRunId/:blockLabel/debug",
|
||||
element: <WorkflowEditor />,
|
||||
element: <WorkflowDebugger />,
|
||||
},
|
||||
{
|
||||
path: "edit",
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Panel,
|
||||
PanOnScrollMode,
|
||||
ReactFlow,
|
||||
Viewport,
|
||||
useEdgesState,
|
||||
useNodesInitialized,
|
||||
useNodesState,
|
||||
@@ -97,6 +98,7 @@ import {
|
||||
startNode,
|
||||
} from "./workflowEditorUtils";
|
||||
import { cn } from "@/util/utils";
|
||||
import { WorkflowDebuggerRun } from "@/routes/workflows/editor/WorkflowDebuggerRun";
|
||||
import { useAutoPan } from "./useAutoPan";
|
||||
|
||||
function convertToParametersYAML(
|
||||
@@ -273,6 +275,13 @@ function FlowRenderer({
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
const [debuggableBlockCount, setDebuggableBlockCount] = useState(0);
|
||||
const nodesInitialized = useNodesInitialized();
|
||||
const [shouldConstrainPan, setShouldConstrainPan] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesInitialized) {
|
||||
setShouldConstrainPan(true);
|
||||
}
|
||||
}, [nodesInitialized]);
|
||||
const { hasChanges, setHasChanges } = useWorkflowHasChangesStore();
|
||||
useShouldNotifyWhenClosingTab(hasChanges);
|
||||
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
|
||||
@@ -625,7 +634,8 @@ function FlowRenderer({
|
||||
}
|
||||
});
|
||||
|
||||
const constrainPan = (y: number) => {
|
||||
const constrainPan = (viewport: Viewport) => {
|
||||
const y = viewport.y;
|
||||
const yLockMin = nodes.reduce(
|
||||
(acc, node) => {
|
||||
const nodeBottom = node.position.y + (node.height ?? 0);
|
||||
@@ -636,15 +646,22 @@ function FlowRenderer({
|
||||
},
|
||||
{ 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,
|
||||
});
|
||||
|
||||
// avoid infinite recursion with onMove
|
||||
if (
|
||||
viewport.x !== xLock ||
|
||||
viewport.y !== newY ||
|
||||
viewport.zoom !== zoomLock
|
||||
) {
|
||||
reactFlowInstance.setViewport({
|
||||
x: xLock,
|
||||
y: newY,
|
||||
zoom: zoomLock,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -734,14 +751,15 @@ function FlowRenderer({
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
colorMode="dark"
|
||||
fitView={!debugStore.isDebugMode}
|
||||
fitView={true}
|
||||
fitViewOptions={{
|
||||
maxZoom: 1,
|
||||
}}
|
||||
deleteKeyCode={null}
|
||||
onMove={(_, viewport) => {
|
||||
const y = viewport.y;
|
||||
debugStore.isDebugMode && constrainPan(y);
|
||||
if (debugStore.isDebugMode && shouldConstrainPan) {
|
||||
constrainPan(viewport);
|
||||
}
|
||||
}}
|
||||
maxZoom={debugStore.isDebugMode ? 1 : 2}
|
||||
minZoom={debugStore.isDebugMode ? 1 : 0.5}
|
||||
@@ -758,6 +776,18 @@ function FlowRenderer({
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
||||
<Controls position="bottom-left" />
|
||||
{debugStore.isDebugMode && (
|
||||
<Panel
|
||||
position="top-right"
|
||||
className="!bottom-[1rem] !right-[1.5rem] !top-0"
|
||||
>
|
||||
<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 mt-[8.5rem] h-full w-full overflow-hidden rounded-xl border-2 border-slate-500">
|
||||
<WorkflowDebuggerRun />
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
<Panel position="top-center" className={cn("h-20")}>
|
||||
<WorkflowHeader
|
||||
debuggableBlockCount={debuggableBlockCount}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { BrowserStream } from "@/components/BrowserStream";
|
||||
import { FloatingWindow } from "@/components/FloatingWindow";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
||||
import { useSidebarStore } from "@/store/SidebarStore";
|
||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||
import { WorkflowSettings } from "../types/workflowTypes";
|
||||
import { FlowRenderer } from "./FlowRenderer";
|
||||
import { getElements } from "./workflowEditorUtils";
|
||||
import { getInitialParameters } from "./utils";
|
||||
|
||||
function WorkflowDebugger() {
|
||||
const { workflowPermanentId } = useParams();
|
||||
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
const { data: workflow } = useWorkflowQuery({
|
||||
workflowPermanentId,
|
||||
});
|
||||
|
||||
const setCollapsed = useSidebarStore((state) => {
|
||||
return state.setCollapsed;
|
||||
});
|
||||
|
||||
const setHasChanges = useWorkflowHasChangesStore(
|
||||
(state) => state.setHasChanges,
|
||||
);
|
||||
|
||||
useMountEffect(() => {
|
||||
setCollapsed(true);
|
||||
setHasChanges(false);
|
||||
});
|
||||
|
||||
if (!workflow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const settings: WorkflowSettings = {
|
||||
persistBrowserSession: workflow.persist_browser_session,
|
||||
proxyLocation: workflow.proxy_location,
|
||||
webhookCallbackUrl: workflow.webhook_callback_url,
|
||||
model: workflow.model,
|
||||
maxScreenshotScrolls: workflow.max_screenshot_scrolls,
|
||||
extraHttpHeaders: workflow.extra_http_headers
|
||||
? JSON.stringify(workflow.extra_http_headers)
|
||||
: null,
|
||||
};
|
||||
|
||||
const elements = getElements(
|
||||
workflow.workflow_definition.blocks,
|
||||
settings,
|
||||
true,
|
||||
);
|
||||
|
||||
const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null;
|
||||
const interactor = workflowRun && isFinalized === false ? "agent" : "human";
|
||||
const browserTitle = interactor === "agent" ? `Browser [🤖]` : `Browser [👤]`;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen w-full">
|
||||
<ReactFlowProvider>
|
||||
<FlowRenderer
|
||||
initialEdges={elements.edges}
|
||||
initialNodes={elements.nodes}
|
||||
initialParameters={getInitialParameters(workflow)}
|
||||
initialTitle={workflow.title}
|
||||
workflow={workflow}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
|
||||
{workflowRun && (
|
||||
<FloatingWindow
|
||||
title={browserTitle}
|
||||
bounded={false}
|
||||
initialWidth={512}
|
||||
initialHeight={360}
|
||||
showMaximizeButton={true}
|
||||
showMinimizeButton={true}
|
||||
showReloadButton={true}
|
||||
>
|
||||
{workflowRun && workflowRun.browser_session_id ? (
|
||||
<BrowserStream
|
||||
interactive={interactor === "human"}
|
||||
browserSessionId={workflowRun.browser_session_id}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="h-full w-full" />
|
||||
)}
|
||||
</FloatingWindow>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowDebugger };
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { WorkflowDebuggerRunTimeline } from "./WorkflowDebuggerRunTimeline";
|
||||
|
||||
function WorkflowDebuggerRun() {
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
|
||||
const workflowFailureReason = workflowRun?.failure_reason ? (
|
||||
<div
|
||||
className="m-4 w-full rounded-md border border-red-600 p-4"
|
||||
style={{
|
||||
backgroundColor: "rgba(220, 38, 38, 0.10)",
|
||||
width: "calc(100% - 2rem)",
|
||||
}}
|
||||
>
|
||||
<div className="font-bold">Workflow Failure Reason</div>
|
||||
<div className="text-sm">{workflowRun.failure_reason}</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowDebuggerRun };
|
||||
@@ -0,0 +1,129 @@
|
||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { statusIsFinalized } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
|
||||
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
|
||||
import {
|
||||
isBlockItem,
|
||||
isObserverThought,
|
||||
isTaskVariantBlockItem,
|
||||
isThoughtItem,
|
||||
ObserverThought,
|
||||
WorkflowRunBlock,
|
||||
} from "../types/workflowRunTypes";
|
||||
import { ThoughtCard } from "@/routes/workflows/workflowRun/ThoughtCard";
|
||||
import {
|
||||
ActionItem,
|
||||
WorkflowRunOverviewActiveElement,
|
||||
} from "@/routes/workflows/workflowRun/WorkflowRunOverview";
|
||||
import { WorkflowRunTimelineBlockItem } from "@/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem";
|
||||
|
||||
type Props = {
|
||||
activeItem: WorkflowRunOverviewActiveElement;
|
||||
onObserverThoughtCardSelected: (item: ObserverThought) => void;
|
||||
onActionItemSelected: (item: ActionItem) => void;
|
||||
onBlockItemSelected: (item: WorkflowRunBlock) => void;
|
||||
};
|
||||
|
||||
function WorkflowDebuggerRunTimeline({
|
||||
activeItem,
|
||||
onObserverThoughtCardSelected,
|
||||
onActionItemSelected,
|
||||
onBlockItemSelected,
|
||||
}: Props) {
|
||||
const { data: workflowRun, isLoading: workflowRunIsLoading } =
|
||||
useWorkflowRunQuery();
|
||||
|
||||
const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } =
|
||||
useWorkflowRunTimelineQuery();
|
||||
|
||||
if (workflowRunIsLoading || workflowRunTimelineIsLoading) {
|
||||
return <Skeleton className="h-full w-full" />;
|
||||
}
|
||||
|
||||
if (!workflowRun || !workflowRunTimeline) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded-xl bg-[#020817] p-12">
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4">
|
||||
<div>
|
||||
Hi! 👋 We're experimenting with a new feature called debugger.
|
||||
</div>
|
||||
<div>
|
||||
This debugger allows you to see the state of your workflow in a live
|
||||
browser.
|
||||
</div>
|
||||
<div>
|
||||
You can run individual blocks, instead of the whole workflow.
|
||||
</div>
|
||||
<div>
|
||||
To get started, press the play button on a block in your workflow.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
|
||||
|
||||
const numberOfActions = workflowRunTimeline.reduce((total, current) => {
|
||||
if (isTaskVariantBlockItem(current)) {
|
||||
return total + current.block!.actions!.length;
|
||||
}
|
||||
return total + 0;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-0 space-y-4 rounded bg-slate-elevation1 p-4">
|
||||
<div className="grid w-full grid-cols-2 gap-2">
|
||||
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||
Actions: {numberOfActions}
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||
Steps: {workflowRun.total_steps ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
{!workflowRunIsFinalized && workflowRunTimeline.length === 0 && (
|
||||
<Skeleton className="h-full w-full" />
|
||||
)}
|
||||
<ScrollArea>
|
||||
<ScrollAreaViewport className="h-full w-full">
|
||||
<div className="w-full space-y-4">
|
||||
{workflowRunIsFinalized && workflowRunTimeline.length === 0 && (
|
||||
<div>Workflow timeline is empty</div>
|
||||
)}
|
||||
{workflowRunTimeline?.map((timelineItem) => {
|
||||
if (isBlockItem(timelineItem)) {
|
||||
return (
|
||||
<WorkflowRunTimelineBlockItem
|
||||
key={timelineItem.block.workflow_run_block_id}
|
||||
subItems={timelineItem.children}
|
||||
activeItem={activeItem}
|
||||
block={timelineItem.block}
|
||||
onActionClick={onActionItemSelected}
|
||||
onBlockItemClick={onBlockItemSelected}
|
||||
onThoughtCardClick={onObserverThoughtCardSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isThoughtItem(timelineItem)) {
|
||||
return (
|
||||
<ThoughtCard
|
||||
key={timelineItem.thought.thought_id}
|
||||
active={
|
||||
isObserverThought(activeItem) &&
|
||||
activeItem.thought_id === timelineItem.thought.thought_id
|
||||
}
|
||||
onClick={onObserverThoughtCardSelected}
|
||||
thought={timelineItem.thought}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</ScrollAreaViewport>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowDebuggerRunTimeline };
|
||||
@@ -7,21 +7,11 @@ 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,
|
||||
WorkflowParameterTypes,
|
||||
WorkflowParameterValueType,
|
||||
WorkflowSettings,
|
||||
} from "../types/workflowTypes";
|
||||
import { ParametersState } from "./types";
|
||||
import { WorkflowSettings } from "../types/workflowTypes";
|
||||
import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery";
|
||||
import { WorkflowDebugOverviewWindow } from "./panels/WorkflowDebugOverviewWindow";
|
||||
import { cn } from "@/util/utils";
|
||||
import { getInitialParameters } from "./utils";
|
||||
|
||||
function WorkflowEditor() {
|
||||
const debugStore = useDebugStore();
|
||||
const { workflowPermanentId } = useParams();
|
||||
const setCollapsed = useSidebarStore((state) => {
|
||||
return state.setCollapsed;
|
||||
@@ -76,123 +66,17 @@ function WorkflowEditor() {
|
||||
!isGlobalWorkflow,
|
||||
);
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
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>
|
||||
)}
|
||||
<ReactFlowProvider>
|
||||
<FlowRenderer
|
||||
initialEdges={elements.edges}
|
||||
initialNodes={elements.nodes}
|
||||
initialParameters={getInitialParameters(workflow)}
|
||||
initialTitle={workflow.title}
|
||||
workflow={workflow}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ function WorkflowHeader({
|
||||
}}
|
||||
>
|
||||
<Crosshair1Icon className="mr-2 h-6 w-6" />
|
||||
{debugStore.isDebugMode ? "End" : "Start Debugging"}
|
||||
{debugStore.isDebugMode ? "End Debugging" : "Start Debugging"}
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
|
||||
@@ -306,7 +306,6 @@ function NodeHeader({
|
||||
title: "Workflow Canceled",
|
||||
description: "The workflow has been successfully canceled.",
|
||||
});
|
||||
navigate(`/workflows/${workflowPermanentId}/debug`);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* NOTE(jdo): this is a hack: we are iframe-ing the overview page, but we really
|
||||
* need dedicated UI component for this.
|
||||
*/
|
||||
|
||||
import { FloatingWindow } from "@/components/FloatingWindow";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function WorkflowDebugOverviewWindow() {
|
||||
return (
|
||||
<FloatingWindow
|
||||
title="Live View"
|
||||
initialWidth={256}
|
||||
initialHeight={512}
|
||||
maximized={true}
|
||||
showMaximizeButton={true}
|
||||
>
|
||||
<WorkflowDebugOverviewWindowIframe />
|
||||
</FloatingWindow>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
) : (
|
||||
// waving hand emoji
|
||||
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 overflow-y-auto rounded-xl bg-[#020817] p-6">
|
||||
<div className="flex h-full w-full max-w-[15rem] flex-col items-center justify-center gap-4 rounded-xl bg-[#020817] p-6">
|
||||
<div>
|
||||
Hi! 👋 We're experimenting with a new feature called debugger.
|
||||
</div>
|
||||
<div>
|
||||
This debugger allows you to see the state of your workflow in a live
|
||||
browser.
|
||||
</div>
|
||||
<div>You can run individual blocks, instead of the whole workflow.</div>
|
||||
<div>
|
||||
To get started, press the play button on a block in your workflow.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowDebugOverviewWindow };
|
||||
100
skyvern-frontend/src/routes/workflows/editor/utils.ts
Normal file
100
skyvern-frontend/src/routes/workflows/editor/utils.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
|
||||
import {
|
||||
isDisplayedInWorkflowEditor,
|
||||
WorkflowEditorParameterTypes,
|
||||
WorkflowParameterTypes,
|
||||
WorkflowParameterValueType,
|
||||
} from "../types/workflowTypes";
|
||||
import { ParametersState } from "./types";
|
||||
|
||||
const getInitialParameters = (workflow: WorkflowApiResponse) => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
export { getInitialParameters };
|
||||
Reference in New Issue
Block a user