Jon/debugger layout (#3340)
This commit is contained in:
@@ -328,7 +328,7 @@ function BrowserStream({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("browser-stream", {
|
className={cn("browser-stream flex items-center justify-center", {
|
||||||
"user-is-controlling": theUserIsControlling,
|
"user-is-controlling": theUserIsControlling,
|
||||||
})}
|
})}
|
||||||
ref={setCanvasContainerRef}
|
ref={setCanvasContainerRef}
|
||||||
@@ -369,7 +369,7 @@ function BrowserStream({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isVncConnected && (
|
{!isVncConnected && (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2 pb-2 pt-4 text-sm text-slate-400">
|
<div className="flex aspect-video w-full flex-col items-center justify-center gap-2 rounded-md border border-slate-800 text-sm text-slate-400">
|
||||||
<RotateThrough interval={7 * 1000}>
|
<RotateThrough interval={7 * 1000}>
|
||||||
<span>Hm, working on the connection...</span>
|
<span>Hm, working on the connection...</span>
|
||||||
<span>Hang tight, we're almost there...</span>
|
<span>Hang tight, we're almost there...</span>
|
||||||
|
|||||||
@@ -756,4 +756,4 @@ function FloatingWindow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { FloatingWindow };
|
export { BreakoutButton, FloatingWindow, PowerButton, ReloadButton };
|
||||||
|
|||||||
31
skyvern-frontend/src/components/Tip.tsx
Normal file
31
skyvern-frontend/src/components/Tip.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
function Tip({
|
||||||
|
asChild = true,
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
}: {
|
||||||
|
asChild?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
content: string | null;
|
||||||
|
}) {
|
||||||
|
if (content === null) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={300}>
|
||||||
|
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[250px]">{content}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tip };
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0.5rem;
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
transition: padding 0.2s ease-in-out;
|
transition: padding 0.2s ease-in-out;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { ActionType, ReadableActionTypes } from "@/api/types";
|
||||||
|
import {
|
||||||
|
CheckCircledIcon,
|
||||||
|
CursorArrowIcon,
|
||||||
|
InputIcon,
|
||||||
|
QuestionMarkIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { Tip } from "@/components/Tip";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
actionType: ActionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons: Partial<Record<ActionType, React.ReactNode>> = {
|
||||||
|
click: <CursorArrowIcon className="h-4 w-4" />,
|
||||||
|
complete: <CheckCircledIcon className="h-4 w-4" />,
|
||||||
|
input_text: <InputIcon className="h-4 w-4" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ActionTypePillMinimal({ actionType }: Props) {
|
||||||
|
const icon = icons[actionType] ?? <QuestionMarkIcon className="h-4 w-4" />;
|
||||||
|
|
||||||
|
if (!icon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tip content={ReadableActionTypes[actionType]}>
|
||||||
|
<div className="flex w-full items-center justify-center gap-2">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</Tip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ActionTypePillMinimal };
|
||||||
@@ -6,7 +6,7 @@ function DebuggerRun() {
|
|||||||
|
|
||||||
const workflowFailureReason = workflowRun?.failure_reason ? (
|
const workflowFailureReason = workflowRun?.failure_reason ? (
|
||||||
<div
|
<div
|
||||||
className="align-self-start h-[8rem] w-full overflow-y-auto rounded-md border border-red-600 p-4"
|
className="align-self-start h-[8rem] min-h-[8rem] w-full overflow-y-auto rounded-md border border-red-600 p-4"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "rgba(220, 38, 38, 0.10)",
|
backgroundColor: "rgba(220, 38, 38, 0.10)",
|
||||||
width: "calc(100% - 2rem)",
|
width: "calc(100% - 2rem)",
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { CrossCircledIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||||
|
import { DebuggerRunTimelineMinimal } from "./DebuggerRunTimelineMinimal";
|
||||||
|
import { Tip } from "@/components/Tip";
|
||||||
|
|
||||||
|
function DebuggerRunMinimal() {
|
||||||
|
const { data: workflowRun } = useWorkflowRunQuery();
|
||||||
|
|
||||||
|
const workflowFailureReason = workflowRun?.failure_reason ? (
|
||||||
|
<Tip content={workflowRun.failure_reason}>
|
||||||
|
<div className="items-center-justify-center flex text-destructive">
|
||||||
|
<CrossCircledIcon />
|
||||||
|
</div>
|
||||||
|
</Tip>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex h-full w-full flex-col items-center justify-start gap-4 overflow-hidden overflow-y-auto pb-12 pt-4">
|
||||||
|
{workflowFailureReason}
|
||||||
|
<div className="flex h-full w-full items-start justify-center">
|
||||||
|
<DebuggerRunTimelineMinimal />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DebuggerRunMinimal };
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { statusIsFinalized } from "@/routes/tasks/types";
|
import { statusIsFinalized } from "@/routes/tasks/types";
|
||||||
@@ -18,7 +17,6 @@ import {
|
|||||||
WorkflowRunOverviewActiveElement,
|
WorkflowRunOverviewActiveElement,
|
||||||
} from "@/routes/workflows/workflowRun/WorkflowRunOverview";
|
} from "@/routes/workflows/workflowRun/WorkflowRunOverview";
|
||||||
import { WorkflowRunTimelineBlockItem } from "@/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem";
|
import { WorkflowRunTimelineBlockItem } from "@/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem";
|
||||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeItem: WorkflowRunOverviewActiveElement;
|
activeItem: WorkflowRunOverviewActiveElement;
|
||||||
@@ -27,28 +25,12 @@ type Props = {
|
|||||||
onBlockItemSelected: (item: WorkflowRunBlock) => void;
|
onBlockItemSelected: (item: WorkflowRunBlock) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Step({ n, children }: { n: number; children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center justify-center gap-2 rounded-lg border border-slate-600 p-4">
|
|
||||||
<div className="absolute right-[-1.22rem] top-[-1.22rem] flex h-[3rem] w-[3rem] items-center justify-center rounded-full border border-slate-600 bg-slate-elevation3 px-4 py-3 text-xl font-bold">
|
|
||||||
{n}
|
|
||||||
</div>
|
|
||||||
<div className="absolute right-[-1.25rem] top-[-1.25rem] flex h-[3rem] w-[3rem] items-center justify-center rounded-full bg-slate-elevation3 px-4 py-3 text-xl font-bold text-slate-100">
|
|
||||||
{n}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DebuggerRunTimeline({
|
function DebuggerRunTimeline({
|
||||||
activeItem,
|
activeItem,
|
||||||
onObserverThoughtCardSelected,
|
onObserverThoughtCardSelected,
|
||||||
onActionItemSelected,
|
onActionItemSelected,
|
||||||
onBlockItemSelected,
|
onBlockItemSelected,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { workflowPermanentId } = useParams();
|
|
||||||
const { data: workflow } = useWorkflowQuery({ workflowPermanentId }!);
|
|
||||||
const { data: workflowRun, isLoading: workflowRunIsLoading } =
|
const { data: workflowRun, isLoading: workflowRunIsLoading } =
|
||||||
useWorkflowRunQuery();
|
useWorkflowRunQuery();
|
||||||
|
|
||||||
@@ -59,67 +41,8 @@ function DebuggerRunTimeline({
|
|||||||
return <Skeleton className="h-full w-full" />;
|
return <Skeleton className="h-full w-full" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocks = workflow?.workflow_definition.blocks ?? [];
|
|
||||||
|
|
||||||
const getStarted =
|
|
||||||
blocks.length === 0 ? (
|
|
||||||
<div>
|
|
||||||
Hi! 👋 To get started, add a block to your workflow. You can do that by
|
|
||||||
clicking the round plus button beneath the Start block, on the left
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const runABlock = (
|
|
||||||
<div>
|
|
||||||
To run a single block, click the play button on that block. Skyvern will
|
|
||||||
run the block in the browser, live!
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const adjustBrowser = (
|
|
||||||
<div>
|
|
||||||
Need to adjust the browser to test your block again? You can click around
|
|
||||||
in the browser to bring Skyvern to any page (manually!)
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const parameters = (
|
|
||||||
<div>
|
|
||||||
Want Skyvern to do different things based on your inputs? Use Parameters
|
|
||||||
to specify them and reference them using <code>{"{{ }}"}</code> syntax!
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const addBlocks = (
|
|
||||||
<div>
|
|
||||||
Not finished? Add a block to your workflow by clicking the round plus
|
|
||||||
button before or after any other block.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
getStarted,
|
|
||||||
runABlock,
|
|
||||||
adjustBrowser,
|
|
||||||
getStarted === null ? parameters : null,
|
|
||||||
getStarted === null ? addBlocks : null,
|
|
||||||
].filter((step) => step);
|
|
||||||
|
|
||||||
if (!workflowRun || !workflowRunTimeline) {
|
if (!workflowRun || !workflowRunTimeline) {
|
||||||
return (
|
return null;
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center overflow-y-auto rounded-xl bg-[#020817] p-8 text-slate-300">
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-around gap-4">
|
|
||||||
<div className="text-center text-xl">
|
|
||||||
Build & Debug Complex Browser Automations
|
|
||||||
</div>
|
|
||||||
{steps.map((step, index) => (
|
|
||||||
<Step key={index} n={index + 1}>
|
|
||||||
{step}
|
|
||||||
</Step>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
|
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
|
||||||
@@ -132,7 +55,7 @@ function DebuggerRunTimeline({
|
|||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-w-0 space-y-4 rounded bg-slate-elevation1 p-4">
|
<div className="w-full min-w-0 space-y-4 rounded p-4">
|
||||||
<div className="grid w-full grid-cols-2 gap-2">
|
<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">
|
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||||
Actions: {numberOfActions}
|
Actions: {numberOfActions}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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, isThoughtItem } from "../types/workflowRunTypes";
|
||||||
|
import { ThoughtCardMinimal } from "@/routes/workflows/workflowRun/ThoughtCardMinimal";
|
||||||
|
import { WorkflowRunTimelineBlockItemMinimal } from "@/routes/workflows/workflowRun/WorkflowRunTimelineBlockItemMinimal";
|
||||||
|
|
||||||
|
function DebuggerRunTimelineMinimal() {
|
||||||
|
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 null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
{!workflowRunIsFinalized && workflowRunTimeline.length === 0 && (
|
||||||
|
<Skeleton className="h-full w-full" />
|
||||||
|
)}
|
||||||
|
<ScrollArea className="h-full w-full">
|
||||||
|
<ScrollAreaViewport className="h-full w-full">
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-4 pt-2">
|
||||||
|
{workflowRunIsFinalized && workflowRunTimeline.length === 0 && (
|
||||||
|
<div>-</div>
|
||||||
|
)}
|
||||||
|
{workflowRunTimeline?.map((timelineItem) => {
|
||||||
|
if (isBlockItem(timelineItem)) {
|
||||||
|
return (
|
||||||
|
<WorkflowRunTimelineBlockItemMinimal
|
||||||
|
key={timelineItem.block.workflow_run_block_id}
|
||||||
|
subItems={timelineItem.children}
|
||||||
|
block={timelineItem.block}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isThoughtItem(timelineItem)) {
|
||||||
|
return (
|
||||||
|
<ThoughtCardMinimal
|
||||||
|
key={timelineItem.thought.thought_id}
|
||||||
|
thought={timelineItem.thought}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ScrollAreaViewport>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DebuggerRunTimelineMinimal };
|
||||||
@@ -220,6 +220,7 @@ function convertToParametersYAML(
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
hideBackground?: boolean;
|
||||||
nodes: Array<AppNode>;
|
nodes: Array<AppNode>;
|
||||||
edges: Array<Edge>;
|
edges: Array<Edge>;
|
||||||
setNodes: (nodes: Array<AppNode>) => void;
|
setNodes: (nodes: Array<AppNode>) => void;
|
||||||
@@ -235,6 +236,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function FlowRenderer({
|
function FlowRenderer({
|
||||||
|
hideBackground = false,
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
setNodes,
|
setNodes,
|
||||||
@@ -648,7 +650,7 @@ function FlowRenderer({
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
colorMode="dark"
|
// colorMode="dark"
|
||||||
fitView={true}
|
fitView={true}
|
||||||
fitViewOptions={{
|
fitViewOptions={{
|
||||||
maxZoom: 1,
|
maxZoom: 1,
|
||||||
@@ -668,7 +670,9 @@ function FlowRenderer({
|
|||||||
zoomOnPinch={!flowIsConstrained}
|
zoomOnPinch={!flowIsConstrained}
|
||||||
zoomOnScroll={!flowIsConstrained}
|
zoomOnScroll={!flowIsConstrained}
|
||||||
>
|
>
|
||||||
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
{!hideBackground && (
|
||||||
|
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
||||||
|
)}
|
||||||
<Controls position="bottom-left" />
|
<Controls position="bottom-left" />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</BlockActionContext.Provider>
|
</BlockActionContext.Provider>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
import {
|
||||||
|
ChevronRightIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
ReloadIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
import { useParams, useSearchParams } from "react-router-dom";
|
import { useParams, useSearchParams } from "react-router-dom";
|
||||||
import { useEdgesState, useNodesState, Edge } from "@xyflow/react";
|
import { useEdgesState, useNodesState, Edge } from "@xyflow/react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -10,7 +15,6 @@ import { getClient } from "@/api/AxiosClient";
|
|||||||
import { DebugSessionApiResponse } from "@/api/types";
|
import { DebugSessionApiResponse } from "@/api/types";
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||||
import { useRanker } from "../hooks/useRanker";
|
|
||||||
import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery";
|
import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery";
|
||||||
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
||||||
import { useCacheKeyValuesQuery } from "../hooks/useCacheKeyValuesQuery";
|
import { useCacheKeyValuesQuery } from "../hooks/useCacheKeyValuesQuery";
|
||||||
@@ -19,6 +23,11 @@ import { useSidebarStore } from "@/store/SidebarStore";
|
|||||||
|
|
||||||
import { AnimatedWave } from "@/components/AnimatedWave";
|
import { AnimatedWave } from "@/components/AnimatedWave";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
BreakoutButton,
|
||||||
|
PowerButton,
|
||||||
|
ReloadButton,
|
||||||
|
} from "@/components/FloatingWindow";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -28,21 +37,20 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { SwitchBar } from "@/components/SwitchBar";
|
|
||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { BrowserStream } from "@/components/BrowserStream";
|
import { BrowserStream } from "@/components/BrowserStream";
|
||||||
import { FloatingWindow } from "@/components/FloatingWindow";
|
|
||||||
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
||||||
import { DebuggerRun } from "@/routes/workflows/debugger/DebuggerRun";
|
import { DebuggerRun } from "@/routes/workflows/debugger/DebuggerRun";
|
||||||
|
import { DebuggerRunMinimal } from "@/routes/workflows/debugger/DebuggerRunMinimal";
|
||||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||||
import { DebuggerRunOutput } from "@/routes/workflows/debugger/DebuggerRunOutput";
|
|
||||||
import { DebuggerPostRunParameters } from "@/routes/workflows/debugger/DebuggerPostRunParameters";
|
|
||||||
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
import {
|
import {
|
||||||
useWorkflowHasChangesStore,
|
useWorkflowHasChangesStore,
|
||||||
useWorkflowSave,
|
useWorkflowSave,
|
||||||
} from "@/store/WorkflowHasChangesStore";
|
} from "@/store/WorkflowHasChangesStore";
|
||||||
|
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
import { FlowRenderer, type FlowRendererProps } from "./FlowRenderer";
|
import { FlowRenderer, type FlowRendererProps } from "./FlowRenderer";
|
||||||
import { AppNode, isWorkflowBlockNode, WorkflowBlockNode } from "./nodes";
|
import { AppNode, isWorkflowBlockNode, WorkflowBlockNode } from "./nodes";
|
||||||
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
||||||
@@ -60,6 +68,8 @@ import {
|
|||||||
} from "./workflowEditorUtils";
|
} from "./workflowEditorUtils";
|
||||||
import { constructCacheKeyValue } from "./utils";
|
import { constructCacheKeyValue } from "./utils";
|
||||||
|
|
||||||
|
import "./workspace-styles.css";
|
||||||
|
|
||||||
const Constants = {
|
const Constants = {
|
||||||
NewBrowserCooldown: 30000,
|
NewBrowserCooldown: 30000,
|
||||||
} as const;
|
} as const;
|
||||||
@@ -85,10 +95,10 @@ function Workspace({
|
|||||||
showBrowser = false,
|
showBrowser = false,
|
||||||
workflow,
|
workflow,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { blockLabel, workflowPermanentId, workflowRunId } = useParams();
|
const { blockLabel, workflowPermanentId } = useParams();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const cacheKeyValueParam = searchParams.get("cache-key-value");
|
const cacheKeyValueParam = searchParams.get("cache-key-value");
|
||||||
const [content, setContent] = useState("actions");
|
const [timelineMode, setTimelineMode] = useState("narrow");
|
||||||
const [cacheKeyValueFilter, setCacheKeyValueFilter] = useState<string | null>(
|
const [cacheKeyValueFilter, setCacheKeyValueFilter] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -102,7 +112,6 @@ function Workspace({
|
|||||||
const { data: workflowRun } = useWorkflowRunQuery();
|
const { data: workflowRun } = useWorkflowRunQuery();
|
||||||
const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null;
|
const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null;
|
||||||
const interactor = workflowRun && isFinalized === false ? "agent" : "human";
|
const interactor = workflowRun && isFinalized === false ? "agent" : "human";
|
||||||
const browserTitle = interactor === "agent" ? `Browser [🤖]` : `Browser [👤]`;
|
|
||||||
|
|
||||||
const [openCycleBrowserDialogue, setOpenCycleBrowserDialogue] =
|
const [openCycleBrowserDialogue, setOpenCycleBrowserDialogue] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@@ -116,36 +125,12 @@ function Workspace({
|
|||||||
const [activeDebugSession, setActiveDebugSession] =
|
const [activeDebugSession, setActiveDebugSession] =
|
||||||
useState<DebugSessionApiResponse | null>(null);
|
useState<DebugSessionApiResponse | null>(null);
|
||||||
const [showPowerButton, setShowPowerButton] = useState(true);
|
const [showPowerButton, setShowPowerButton] = useState(true);
|
||||||
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
|
const [isReloading, setIsReloading] = useState(false);
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
|
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
|
||||||
const blockScriptStore = useBlockScriptStore();
|
const blockScriptStore = useBlockScriptStore();
|
||||||
const { rankedItems, promote } = useRanker([
|
|
||||||
"browserWindow",
|
|
||||||
"header",
|
|
||||||
"dropdown",
|
|
||||||
"history",
|
|
||||||
"infiniteCanvas",
|
|
||||||
]);
|
|
||||||
const [hideControlButtons, setHideControlButtons] = useState(false);
|
|
||||||
|
|
||||||
// ---start fya: https://github.com/frontyardart
|
|
||||||
const hasForLoopNode = nodes.some((node) => node.type === "loop");
|
|
||||||
|
|
||||||
const initialBrowserPosition = {
|
|
||||||
x: hasForLoopNode ? 600 : 520,
|
|
||||||
y: 132,
|
|
||||||
};
|
|
||||||
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
const rightPadding = 567;
|
|
||||||
const initialWidth = Math.max(
|
|
||||||
512,
|
|
||||||
windowWidth - initialBrowserPosition.x - rightPadding,
|
|
||||||
);
|
|
||||||
const initialHeight = (initialWidth / 16) * 9;
|
|
||||||
// ---end fya
|
|
||||||
|
|
||||||
const cacheKey = workflow?.cache_key ?? "";
|
const cacheKey = workflow?.cache_key ?? "";
|
||||||
|
|
||||||
const [cacheKeyValue, setCacheKeyValue] = useState(
|
const [cacheKeyValue, setCacheKeyValue] = useState(
|
||||||
@@ -156,6 +141,18 @@ function Workspace({
|
|||||||
: constructCacheKeyValue(cacheKey, workflow),
|
: constructCacheKeyValue(cacheKey, workflow),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
setTimelineMode("narrow");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentUrlValue = searchParams.get("cache-key-value");
|
const currentUrlValue = searchParams.get("cache-key-value");
|
||||||
const targetValue = cacheKeyValue === "" ? null : cacheKeyValue;
|
const targetValue = cacheKeyValue === "" ? null : cacheKeyValue;
|
||||||
@@ -202,10 +199,13 @@ function Workspace({
|
|||||||
|
|
||||||
const workflowChangesStore = useWorkflowHasChangesStore();
|
const workflowChangesStore = useWorkflowHasChangesStore();
|
||||||
|
|
||||||
|
const showBreakoutButton =
|
||||||
|
activeDebugSession && activeDebugSession.browser_session_id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a new tab (not window) with the browser session URL.
|
* Open a new tab (not window) with the browser session URL.
|
||||||
*/
|
*/
|
||||||
const handleOnBreakout = () => {
|
const breakout = () => {
|
||||||
if (activeDebugSession) {
|
if (activeDebugSession) {
|
||||||
const pbsId = activeDebugSession.browser_session_id;
|
const pbsId = activeDebugSession.browser_session_id;
|
||||||
if (pbsId) {
|
if (pbsId) {
|
||||||
@@ -214,10 +214,23 @@ function Workspace({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnCycle = () => {
|
const cycle = () => {
|
||||||
setOpenCycleBrowserDialogue(true);
|
setOpenCycleBrowserDialogue(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
if (isReloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReloadKey((prev) => prev + 1);
|
||||||
|
setIsReloading(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsReloading(false);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
useMountEffect(() => {
|
useMountEffect(() => {
|
||||||
setCollapsed(true);
|
setCollapsed(true);
|
||||||
workflowChangesStore.setHasChanges(false);
|
workflowChangesStore.setHasChanges(false);
|
||||||
@@ -459,7 +472,6 @@ function Workspace({
|
|||||||
active: true,
|
active: true,
|
||||||
content: "cacheKeyValues",
|
content: "cacheKeyValues",
|
||||||
});
|
});
|
||||||
promote("dropdown");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleCacheKeyValuesPanel() {
|
function toggleCacheKeyValuesPanel() {
|
||||||
@@ -468,7 +480,6 @@ function Workspace({
|
|||||||
workflowPanelState.content === "cacheKeyValues"
|
workflowPanelState.content === "cacheKeyValues"
|
||||||
) {
|
) {
|
||||||
closeWorkflowPanel();
|
closeWorkflowPanel();
|
||||||
promote("header");
|
|
||||||
} else {
|
} else {
|
||||||
openCacheKeyValuesPanel();
|
openCacheKeyValuesPanel();
|
||||||
}
|
}
|
||||||
@@ -581,13 +592,7 @@ function Workspace({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* header panel */}
|
{/* header panel */}
|
||||||
<div
|
<div className="absolute left-6 right-6 top-8 z-10 h-20">
|
||||||
className="absolute left-6 right-6 top-8 h-20"
|
|
||||||
style={{ zIndex: rankedItems.header ?? 3 }}
|
|
||||||
onMouseDownCapture={() => {
|
|
||||||
promote("header");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<WorkflowHeader
|
<WorkflowHeader
|
||||||
cacheKeyValue={cacheKeyValue}
|
cacheKeyValue={cacheKeyValue}
|
||||||
cacheKeyValues={cacheKeyValues}
|
cacheKeyValues={cacheKeyValues}
|
||||||
@@ -631,13 +636,11 @@ function Workspace({
|
|||||||
workflowPanelState.content === "parameters"
|
workflowPanelState.content === "parameters"
|
||||||
) {
|
) {
|
||||||
closeWorkflowPanel();
|
closeWorkflowPanel();
|
||||||
promote("header");
|
|
||||||
} else {
|
} else {
|
||||||
setWorkflowPanelState({
|
setWorkflowPanelState({
|
||||||
active: true,
|
active: true,
|
||||||
content: "parameters",
|
content: "parameters",
|
||||||
});
|
});
|
||||||
promote("dropdown");
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSave={async () => {
|
onSave={async () => {
|
||||||
@@ -666,13 +669,12 @@ function Workspace({
|
|||||||
}}
|
}}
|
||||||
onRun={() => {
|
onRun={() => {
|
||||||
closeWorkflowPanel();
|
closeWorkflowPanel();
|
||||||
promote("header");
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* sub panels */}
|
{/* sub panels in design mode */}
|
||||||
{workflowPanelState.active && (
|
{!showBrowser && workflowPanelState.active && (
|
||||||
<div
|
<div
|
||||||
className="absolute right-6 top-[8.5rem]"
|
className="absolute right-6 top-[8.5rem]"
|
||||||
style={{
|
style={{
|
||||||
@@ -680,11 +682,8 @@ function Workspace({
|
|||||||
workflowPanelState.content === "nodeLibrary"
|
workflowPanelState.content === "nodeLibrary"
|
||||||
? "calc(100vh - 9.5rem)"
|
? "calc(100vh - 9.5rem)"
|
||||||
: "unset",
|
: "unset",
|
||||||
zIndex: rankedItems.dropdown ?? 2,
|
|
||||||
}}
|
|
||||||
onMouseDownCapture={() => {
|
|
||||||
promote("dropdown");
|
|
||||||
}}
|
}}
|
||||||
|
onMouseDownCapture={() => {}}
|
||||||
>
|
>
|
||||||
{workflowPanelState.content === "cacheKeyValues" && (
|
{workflowPanelState.content === "cacheKeyValues" && (
|
||||||
<WorkflowCacheKeyValuesPanel
|
<WorkflowCacheKeyValuesPanel
|
||||||
@@ -695,9 +694,6 @@ function Workspace({
|
|||||||
setToDeleteCacheKeyValue(cacheKeyValue);
|
setToDeleteCacheKeyValue(cacheKeyValue);
|
||||||
setOpenConfirmCacheKeyValueDeleteDialogue(true);
|
setOpenConfirmCacheKeyValueDeleteDialogue(true);
|
||||||
}}
|
}}
|
||||||
onMouseDownCapture={() => {
|
|
||||||
promote("dropdown");
|
|
||||||
}}
|
|
||||||
onPaginate={(page) => {
|
onPaginate={(page) => {
|
||||||
setPage(page);
|
setPage(page);
|
||||||
}}
|
}}
|
||||||
@@ -709,17 +705,10 @@ function Workspace({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{workflowPanelState.content === "parameters" && (
|
{workflowPanelState.content === "parameters" && (
|
||||||
<WorkflowParametersPanel
|
<WorkflowParametersPanel />
|
||||||
onMouseDownCapture={() => {
|
|
||||||
promote("dropdown");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{workflowPanelState.content === "nodeLibrary" && (
|
{workflowPanelState.content === "nodeLibrary" && (
|
||||||
<WorkflowNodeLibraryPanel
|
<WorkflowNodeLibraryPanel
|
||||||
onMouseDownCapture={() => {
|
|
||||||
promote("dropdown");
|
|
||||||
}}
|
|
||||||
onNodeClick={(props) => {
|
onNodeClick={(props) => {
|
||||||
addNode(props);
|
addNode(props);
|
||||||
}}
|
}}
|
||||||
@@ -728,110 +717,189 @@ function Workspace({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showBrowser && (
|
<div className="relative flex h-full w-full overflow-hidden overflow-x-hidden">
|
||||||
|
{/* infinite canvas */}
|
||||||
<div
|
<div
|
||||||
className="absolute right-6 top-[8.5rem] h-[calc(100vh-9.5rem)]"
|
className={cn("skyvern-split-left h-full w-[33rem]", {
|
||||||
style={{ zIndex: rankedItems.history ?? 1 }}
|
"w-full": !showBrowser,
|
||||||
onMouseDownCapture={() => {
|
})}
|
||||||
closeWorkflowPanel();
|
|
||||||
promote("history");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="pointer-events-none absolute right-0 top-0 flex h-full w-[400px] flex-col items-end justify-end bg-slate-900">
|
<FlowRenderer
|
||||||
<div className="pointer-events-auto relative flex h-full w-full flex-col items-start overflow-hidden rounded-xl border border-slate-700">
|
hideBackground={showBrowser}
|
||||||
{workflowRunId && (
|
nodes={nodes}
|
||||||
<SwitchBar
|
edges={edges}
|
||||||
className="m-2 border-none"
|
setNodes={setNodes}
|
||||||
onChange={(value) => setContent(value)}
|
setEdges={setEdges}
|
||||||
value={content}
|
onNodesChange={onNodesChange}
|
||||||
options={[
|
onEdgesChange={onEdgesChange}
|
||||||
{
|
initialTitle={initialTitle}
|
||||||
label: "Actions",
|
workflow={workflow}
|
||||||
value: "actions",
|
/>
|
||||||
},
|
</div>
|
||||||
{
|
|
||||||
label: "Inputs",
|
{/* divider if browser is in play */}
|
||||||
value: "inputs",
|
{showBrowser && (
|
||||||
},
|
<div className="mt-[8rem] h-[calc(100%-8rem)] w-[1px] bg-slate-800" />
|
||||||
{
|
)}
|
||||||
label: "Outputs",
|
|
||||||
value: "outputs",
|
{/* browser & timeline & sub-panels in debug mode */}
|
||||||
},
|
{showBrowser && (
|
||||||
]}
|
<div className="skyvern-split-right relative flex h-full flex-1 items-end justify-center bg-[#020617] p-4 pl-6">
|
||||||
/>
|
{/* sub panels */}
|
||||||
)}
|
{workflowPanelState.active && (
|
||||||
<div className="h-full w-full overflow-hidden overflow-y-auto">
|
<div
|
||||||
{(!workflowRunId || content === "actions") && <DebuggerRun />}
|
className={cn("absolute right-6 top-[8.5rem]", {
|
||||||
{workflowRunId && content === "inputs" && (
|
"left-6": workflowPanelState.content === "nodeLibrary",
|
||||||
<DebuggerPostRunParameters />
|
})}
|
||||||
|
style={{
|
||||||
|
top: "10.5rem",
|
||||||
|
height:
|
||||||
|
workflowPanelState.content === "nodeLibrary"
|
||||||
|
? "calc(100vh - 14rem)"
|
||||||
|
: "unset",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{workflowPanelState.content === "cacheKeyValues" && (
|
||||||
|
<WorkflowCacheKeyValuesPanel
|
||||||
|
cacheKeyValues={cacheKeyValues}
|
||||||
|
pending={cacheKeyValuesLoading}
|
||||||
|
scriptKey={workflow.cache_key ?? "default"}
|
||||||
|
onDelete={(cacheKeyValue) => {
|
||||||
|
setToDeleteCacheKeyValue(cacheKeyValue);
|
||||||
|
setOpenConfirmCacheKeyValueDeleteDialogue(true);
|
||||||
|
}}
|
||||||
|
onPaginate={(page) => {
|
||||||
|
setPage(page);
|
||||||
|
}}
|
||||||
|
onSelect={(cacheKeyValue) => {
|
||||||
|
setCacheKeyValue(cacheKeyValue);
|
||||||
|
setCacheKeyValueFilter("");
|
||||||
|
closeWorkflowPanel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{workflowRunId && content === "outputs" && (
|
{workflowPanelState.content === "parameters" && (
|
||||||
<DebuggerRunOutput />
|
<WorkflowParametersPanel />
|
||||||
)}
|
)}
|
||||||
|
{workflowPanelState.content === "nodeLibrary" && (
|
||||||
|
<WorkflowNodeLibraryPanel
|
||||||
|
onNodeClick={(props) => {
|
||||||
|
addNode(props);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* browser & timeline */}
|
||||||
|
<div className="flex h-[calc(100%-8rem)] w-full gap-6">
|
||||||
|
{/* browser */}
|
||||||
|
<div className="flex h-full w-full flex-1 flex-col items-center justify-center">
|
||||||
|
<div key={reloadKey} className="w-full flex-1">
|
||||||
|
{activeDebugSession &&
|
||||||
|
activeDebugSession.browser_session_id &&
|
||||||
|
!cycleBrowser.isPending ? (
|
||||||
|
<BrowserStream
|
||||||
|
interactive={interactor === "human"}
|
||||||
|
browserSessionId={activeDebugSession.browser_session_id}
|
||||||
|
showControlButtons={interactor === "human"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex aspect-video w-full flex-col items-center justify-center gap-2 rounded-md border border-slate-800 pb-2 pt-4 text-sm text-slate-400">
|
||||||
|
Connecting to your browser...
|
||||||
|
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<footer className="flex h-[2rem] w-full items-center justify-start gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GlobeIcon /> Live Browser
|
||||||
|
</div>
|
||||||
|
{showBreakoutButton && (
|
||||||
|
<BreakoutButton onClick={() => breakout()} />
|
||||||
|
)}
|
||||||
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
{showPowerButton && <PowerButton onClick={() => cycle()} />}
|
||||||
|
<ReloadButton
|
||||||
|
isReloading={isReloading}
|
||||||
|
onClick={() => reload()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* timeline */}
|
||||||
|
<div
|
||||||
|
className={cn("h-full w-[5rem] overflow-visible", {
|
||||||
|
"pointer-events-none w-[0px] overflow-hidden": !blockLabel,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative h-full w-[25rem] translate-x-[-20.5rem] bg-[#020617] transition-all",
|
||||||
|
{
|
||||||
|
"translate-x-[0rem]": timelineMode === "narrow",
|
||||||
|
group: timelineMode === "narrow",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (timelineMode === "narrow") {
|
||||||
|
setTimelineMode("wide");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* timeline wide */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute left-[0.5rem] right-0 top-0 flex h-full w-[400px] flex-col items-end justify-end opacity-0 transition-all duration-1000",
|
||||||
|
{ "opacity-100": timelineMode === "wide" },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none relative flex h-full w-full flex-col items-start overflow-hidden bg-[#020617]",
|
||||||
|
{ "pointer-events-auto": timelineMode === "wide" },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DebuggerRun />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* divider */}
|
||||||
|
<div className="vertical-line-gradient absolute left-0 top-0 h-full w-[2px]"></div>
|
||||||
|
|
||||||
|
{/* slide indicator */}
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-0 z-10 flex h-full items-center justify-center p-1 opacity-30 transition-opacity hover:opacity-100 group-hover:opacity-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setTimelineMode(
|
||||||
|
timelineMode === "wide" ? "narrow" : "wide",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timelineMode === "narrow" && <ChevronLeftIcon />}
|
||||||
|
{timelineMode === "wide" && <ChevronRightIcon />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* timeline narrow */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"delay-[300ms] pointer-events-none absolute left-0 top-0 h-full w-[6rem] rounded-l-lg opacity-0 transition-all duration-1000",
|
||||||
|
{
|
||||||
|
"pointer-events-auto opacity-100":
|
||||||
|
timelineMode === "narrow",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DebuggerRunMinimal />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* infinite canvas */}
|
|
||||||
<FlowRenderer
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
setNodes={setNodes}
|
|
||||||
setEdges={setEdges}
|
|
||||||
onNodesChange={onNodesChange}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
initialTitle={initialTitle}
|
|
||||||
workflow={workflow}
|
|
||||||
onMouseDownCapture={() => promote("infiniteCanvas")}
|
|
||||||
zIndex={rankedItems.infiniteCanvas}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* browser */}
|
|
||||||
{showBrowser && (
|
|
||||||
<FloatingWindow
|
|
||||||
title={browserTitle}
|
|
||||||
bounded={false}
|
|
||||||
initialPosition={initialBrowserPosition}
|
|
||||||
initialWidth={initialWidth}
|
|
||||||
initialHeight={initialHeight}
|
|
||||||
showBreakoutButton={activeDebugSession !== null}
|
|
||||||
showMaximizeButton={true}
|
|
||||||
showMinimizeButton={true}
|
|
||||||
showPowerButton={blockLabel === undefined && showPowerButton}
|
|
||||||
showReloadButton={true}
|
|
||||||
zIndex={rankedItems.browserWindow ?? 4}
|
|
||||||
// --
|
|
||||||
onBreakout={handleOnBreakout}
|
|
||||||
onCycle={handleOnCycle}
|
|
||||||
onFocus={() => promote("browserWindow")}
|
|
||||||
onMinimize={() => {
|
|
||||||
setHideControlButtons(true);
|
|
||||||
}}
|
|
||||||
onMaximize={() => {
|
|
||||||
setHideControlButtons(false);
|
|
||||||
}}
|
|
||||||
onRestore={() => {
|
|
||||||
setHideControlButtons(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{activeDebugSession &&
|
|
||||||
activeDebugSession.browser_session_id &&
|
|
||||||
!cycleBrowser.isPending ? (
|
|
||||||
<BrowserStream
|
|
||||||
interactive={false}
|
|
||||||
browserSessionId={activeDebugSession.browser_session_id}
|
|
||||||
showControlButtons={!hideControlButtons}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2 pb-2 pt-4 text-sm text-slate-400">
|
|
||||||
Connecting to your browser...
|
|
||||||
<AnimatedWave text=".‧₊˚ ⋅ ✨★ ‧₊˚ ⋅" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FloatingWindow>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.vertical-line-gradient {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(51, 65, 85, 0.3) 20%,
|
||||||
|
rgba(51, 65, 85, 1) 50%,
|
||||||
|
rgba(51, 65, 85, 0.3) 80%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-gradient-error {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(51, 0, 0, 0.3) 5%,
|
||||||
|
rgba(51, 0, 0, 0.4) 50%,
|
||||||
|
rgba(51, 0, 0, 0.3) 95%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-gradient-success {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0%,
|
||||||
|
rgba(0, 51, 0, 0.3) 5%,
|
||||||
|
rgba(0, 51, 0, 0.4) 50%,
|
||||||
|
rgba(0, 51, 0, 0.3) 95%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { LightningBoltIcon } from "@radix-ui/react-icons";
|
||||||
|
import { ActionsApiResponse, Status } from "@/api/types";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { ActionTypePillMinimal } from "@/routes/tasks/detail/ActionTypePillMinimal";
|
||||||
|
import { ItemStatusIndicator } from "./ItemStatusIndicator";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
action: ActionsApiResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ActionCardMinimal({ action }: Props) {
|
||||||
|
const success =
|
||||||
|
action.status === Status.Completed || action.status === Status.Skipped;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemStatusIndicator failure={!success} success={success} offset="-0.7rem">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<ActionTypePillMinimal actionType={action.action_type} />
|
||||||
|
{action.created_by === "script" && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={300}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<LightningBoltIcon className="h-4 w-4 text-[gold]" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[250px]">
|
||||||
|
Code Execution
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ItemStatusIndicator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ActionCardMinimal };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
offset?: string;
|
||||||
|
failure?: boolean;
|
||||||
|
success?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemStatusIndicator({
|
||||||
|
children,
|
||||||
|
offset = "-0.6rem",
|
||||||
|
failure,
|
||||||
|
success,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center justify-center overflow-visible">
|
||||||
|
{children}
|
||||||
|
{success && (
|
||||||
|
<CheckIcon
|
||||||
|
className="absolute h-3 w-3 text-success"
|
||||||
|
style={{ right: offset, top: offset }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{failure && (
|
||||||
|
<Cross2Icon
|
||||||
|
className="absolute h-[0.65rem] w-[0.65rem] text-destructive"
|
||||||
|
style={{ right: offset, top: offset }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ItemStatusIndicator };
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ObserverThought } from "../types/workflowRunTypes";
|
||||||
|
import { Tip } from "@/components/Tip";
|
||||||
|
import { BrainIcon } from "@/components/icons/BrainIcon";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
thought: ObserverThought;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ThoughtCardMinimal({ thought }: Props) {
|
||||||
|
return (
|
||||||
|
<Tip content={thought.answer || thought.thought || null}>
|
||||||
|
<BrainIcon className="size-6" />
|
||||||
|
</Tip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ThoughtCardMinimal };
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Tip } from "@/components/Tip";
|
||||||
|
import { workflowBlockTitle } from "../editor/nodes/types";
|
||||||
|
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
|
||||||
|
import {
|
||||||
|
isBlockItem,
|
||||||
|
isThoughtItem,
|
||||||
|
WorkflowRunBlock,
|
||||||
|
WorkflowRunTimelineItem,
|
||||||
|
} from "../types/workflowRunTypes";
|
||||||
|
import { ActionCardMinimal } from "./ActionCardMinimal";
|
||||||
|
import { Status } from "@/api/types";
|
||||||
|
import { ThoughtCardMinimal } from "./ThoughtCardMinimal";
|
||||||
|
import { ItemStatusIndicator } from "./ItemStatusIndicator";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
block: WorkflowRunBlock;
|
||||||
|
subItems: Array<WorkflowRunTimelineItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function WorkflowRunTimelineBlockItemMinimal({ block, subItems }: Props) {
|
||||||
|
const actions = block.actions ?? [];
|
||||||
|
const showStatusIndicator = block.status !== null;
|
||||||
|
const showSuccessIndicator =
|
||||||
|
showStatusIndicator && block.status === Status.Completed;
|
||||||
|
const showFailureIndicator =
|
||||||
|
showStatusIndicator &&
|
||||||
|
(block.status === Status.Failed ||
|
||||||
|
block.status === Status.Terminated ||
|
||||||
|
block.status === Status.TimedOut ||
|
||||||
|
block.status === Status.Canceled);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2">
|
||||||
|
<Tip
|
||||||
|
content={workflowBlockTitle[block.block_type] ?? null}
|
||||||
|
asChild={false}
|
||||||
|
>
|
||||||
|
<ItemStatusIndicator
|
||||||
|
failure={showFailureIndicator}
|
||||||
|
success={showSuccessIndicator}
|
||||||
|
>
|
||||||
|
<WorkflowBlockIcon workflowBlockType={block.block_type} />
|
||||||
|
</ItemStatusIndicator>
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
{actions.length > 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-md p-2">
|
||||||
|
{actions.map((action) => {
|
||||||
|
return <ActionCardMinimal key={action.action_id} action={action} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{subItems.map((item) => {
|
||||||
|
if (isBlockItem(item)) {
|
||||||
|
return (
|
||||||
|
<WorkflowRunTimelineBlockItemMinimal
|
||||||
|
key={item.block.workflow_run_block_id}
|
||||||
|
subItems={item.children}
|
||||||
|
block={item.block}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isThoughtItem(item)) {
|
||||||
|
return (
|
||||||
|
<ThoughtCardMinimal
|
||||||
|
key={item.thought.thought_id}
|
||||||
|
thought={item.thought}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WorkflowRunTimelineBlockItemMinimal };
|
||||||
Reference in New Issue
Block a user