Workflow Run Timeline UI (#1433)

This commit is contained in:
Shuchang Zheng
2024-12-23 23:44:47 -08:00
committed by GitHub
parent 682bc717f9
commit 517de67811
42 changed files with 1517 additions and 301 deletions

View File

@@ -223,6 +223,7 @@ export type TaskGenerationApiResponse = {
};
export type ActionsApiResponse = {
action_id: string;
action_type: ActionType;
status: Status;
task_id: string | null;

View File

@@ -0,0 +1,39 @@
import { cn } from "@/util/utils";
import { NavLink } from "react-router-dom";
type Option = {
label: string;
to: string;
};
type Props = {
options: Option[];
};
function SwitchBarNavigation({ options }: Props) {
return (
<div className="flex w-fit gap-2 rounded-sm border border-slate-700 p-2">
{options.map((option) => {
return (
<NavLink
to={option.to}
replace
key={option.to}
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
{option.label}
</NavLink>
);
})}
</div>
);
}
export { SwitchBarNavigation };

View File

@@ -17,10 +17,10 @@ import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters"
import { Workflows } from "./routes/workflows/Workflows";
import { WorkflowsPageLayout } from "./routes/workflows/WorkflowsPageLayout";
import { WorkflowEditor } from "./routes/workflows/editor/WorkflowEditor";
import { WorkflowRunBlocks } from "./routes/workflows/workflowRun/WorkflowRunBlocks";
import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput";
import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/WorkflowPostRunParameters";
import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording";
import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview";
const router = createBrowserRouter([
{
@@ -115,11 +115,11 @@ const router = createBrowserRouter([
children: [
{
index: true,
element: <Navigate to="blocks" />,
element: <Navigate to="overview" />,
},
{
path: "blocks",
element: <WorkflowRunBlocks />,
path: "overview",
element: <WorkflowRunOverview />,
},
{
path: "output",

View File

@@ -19,7 +19,6 @@ import {
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { observerFeatureEnabled } from "@/util/env";
import {
FileTextIcon,
GearIcon,
@@ -233,35 +232,33 @@ function PromptBox() {
placeholder="Enter your prompt..."
rows={1}
/>
{observerFeatureEnabled && (
<Select value={selectValue} onValueChange={setSelectValue}>
<SelectTrigger className="w-48 focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="border-slate-500 bg-slate-elevation3">
<CustomSelectItem value="v1">
<div className="space-y-2">
<div>
<SelectItemText>Skyvern 1.0 (Tasks)</SelectItemText>
</div>
<div className="text-xs text-slate-400">
best for simple tasks
</div>
<Select value={selectValue} onValueChange={setSelectValue}>
<SelectTrigger className="w-48 focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="border-slate-500 bg-slate-elevation3">
<CustomSelectItem value="v1">
<div className="space-y-2">
<div>
<SelectItemText>Skyvern 1.0 (Tasks)</SelectItemText>
</div>
</CustomSelectItem>
<CustomSelectItem value="v2" className="hover:bg-slate-800">
<div className="space-y-2">
<div>
<SelectItemText>Skyvern 2.0 (Observer)</SelectItemText>
</div>
<div className="text-xs text-slate-400">
best for complex tasks
</div>
<div className="text-xs text-slate-400">
best for simple tasks
</div>
</CustomSelectItem>
</SelectContent>
</Select>
)}
</div>
</CustomSelectItem>
<CustomSelectItem value="v2" className="hover:bg-slate-800">
<div className="space-y-2">
<div>
<SelectItemText>Skyvern 2.0 (Observer)</SelectItemText>
</div>
<div className="text-xs text-slate-400">
best for complex tasks
</div>
</div>
</CustomSelectItem>
</SelectContent>
</Select>
<div className="flex items-center">
{startObserverCruiseMutation.isPending ||
getTaskFromPromptMutation.isPending ||
@@ -271,7 +268,7 @@ function PromptBox() {
<PaperPlaneIcon
className="h-6 w-6 cursor-pointer"
onClick={async () => {
if (observerFeatureEnabled && selectValue === "v2") {
if (selectValue === "v2") {
startObserverCruiseMutation.mutate(prompt);
return;
}

View File

@@ -3,7 +3,6 @@ import { ArtifactApiResponse, ArtifactType, Status } from "@/api/types";
import { ZoomableImage } from "@/components/ZoomableImage";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { getImageURL } from "./artifactUtils";
import { ReloadIcon } from "@radix-ui/react-icons";
import { statusIsNotFinalized } from "../types";
@@ -15,15 +14,18 @@ type Props = {
};
function ActionScreenshot({ stepId, index, taskStatus }: Props) {
const { taskId } = useParams();
const credentialGetter = useCredentialGetter();
const { data: artifacts, isLoading } = useQuery<Array<ArtifactApiResponse>>({
queryKey: ["task", taskId, "steps", stepId, "artifacts"],
const {
data: artifacts,
isLoading,
isFetching,
} = useQuery<Array<ArtifactApiResponse>>({
queryKey: ["step", stepId, "artifacts"],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client
.get(`/tasks/${taskId}/steps/${stepId}/artifacts`)
.get(`/step/${stepId}/artifacts`)
.then((response) => response.data);
},
refetchInterval: (query) => {
@@ -46,7 +48,7 @@ function ActionScreenshot({ stepId, index, taskStatus }: Props) {
if (isLoading) {
return (
<div className="mx-auto flex max-h-[400px] flex-col items-center gap-2 overflow-hidden">
<div className="flex h-full items-center justify-center gap-2 bg-slate-elevation1">
<ReloadIcon className="h-6 w-6 animate-spin" />
<div>Loading screenshot...</div>
</div>
@@ -59,14 +61,25 @@ function ActionScreenshot({ stepId, index, taskStatus }: Props) {
statusIsNotFinalized({ status: taskStatus })
) {
return <div>The screenshot for this action is not available yet.</div>;
} else if (isFetching) {
return (
<div className="flex h-full items-center justify-center gap-2 bg-slate-elevation1">
<ReloadIcon className="h-6 w-6 animate-spin" />
<div>Loading screenshot...</div>
</div>
);
}
if (!screenshot) {
return <div>No screenshot found for this action.</div>;
return (
<div className="flex h-full items-center justify-center bg-slate-elevation1">
No screenshot found for this action.
</div>
);
}
return (
<figure className="mx-auto flex max-w-full flex-col items-center gap-2 overflow-hidden">
<figure className="mx-auto flex max-w-full flex-col items-center gap-2 overflow-hidden rounded">
<ZoomableImage src={getImageURL(screenshot)} alt="llm-screenshot" />
</figure>
);

View File

@@ -4,6 +4,8 @@ import {
TaskApiResponse,
WorkflowRunStatusApiResponse,
} from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { SwitchBarNavigation } from "@/components/SwitchBarNavigation";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -18,20 +20,18 @@ import {
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "@/components/ui/use-toast";
import { useApiCredential } from "@/hooks/useApiCredential";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { cn } from "@/util/utils";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
import { copyText } from "@/util/copyText";
import { apiBaseUrl } from "@/util/env";
import { CopyIcon, PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, NavLink, Outlet, useParams } from "react-router-dom";
import { useTaskQuery } from "./hooks/useTaskQuery";
import fetchToCurl from "fetch-to-curl";
import { apiBaseUrl } from "@/util/env";
import { useApiCredential } from "@/hooks/useApiCredential";
import { copyText } from "@/util/copyText";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
import { StatusBadge } from "@/components/StatusBadge";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { Link, Outlet, useParams } from "react-router-dom";
import { statusIsFinalized } from "../types";
import { useTaskQuery } from "./hooks/useTaskQuery";
function createTaskRequestObject(values: TaskApiResponse) {
return {
@@ -257,7 +257,7 @@ function TaskDetails() {
workflow &&
workflowRun && (
<Link
to={`/workflows/${workflow.workflow_permanent_id}/${workflowRun.workflow_run_id}/blocks`}
to={`/workflows/${workflow.workflow_permanent_id}/${workflowRun.workflow_run_id}/overview`}
>
{workflow.title}
</Link>
@@ -274,64 +274,26 @@ function TaskDetails() {
{failureReason}
</>
)}
<div className="flex w-fit gap-2 rounded-sm border border-slate-700 p-2">
<NavLink
to="actions"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Actions
</NavLink>
<NavLink
to="recording"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Recording
</NavLink>
<NavLink
to="parameters"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Parameters
</NavLink>
<NavLink
to="diagnostics"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Diagnostics
</NavLink>
</div>
<SwitchBarNavigation
options={[
{
label: "Actions",
to: "actions",
},
{
label: "Recording",
to: "recording",
},
{
label: "Parameters",
to: "parameters",
},
{
label: "Diagnostics",
to: "diagnostics",
},
]}
/>
<Outlet />
</div>
);

View File

@@ -35,6 +35,14 @@ export function statusIsFinalized({ status }: { status: Status }): boolean {
);
}
export function statusIsAFailureType({ status }: { status: Status }): boolean {
return (
status === Status.Failed ||
status === Status.Terminated ||
status === Status.TimedOut
);
}
export function statusIsRunningOrQueued({
status,
}: {

View File

@@ -135,7 +135,7 @@ function RunWorkflowForm({
<ToastAction altText="View">
<Button asChild>
<Link
to={`/workflows/${workflowPermanentId}/${response.data.workflow_run_id}/blocks`}
to={`/workflows/${workflowPermanentId}/${response.data.workflow_run_id}/overview`}
>
View
</Link>

View File

@@ -139,14 +139,14 @@ function WorkflowPage() {
if (event.ctrlKey || event.metaKey) {
window.open(
window.location.origin +
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/blocks`,
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
"_blank",
"noopener,noreferrer",
);
return;
}
navigate(
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/blocks`,
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
);
}}
className="cursor-pointer"

View File

@@ -1,5 +1,7 @@
import { getClient } from "@/api/AxiosClient";
import { ProxyLocation } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { SwitchBarNavigation } from "@/components/SwitchBarNavigation";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -25,12 +27,10 @@ import {
} from "@radix-ui/react-icons";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import fetchToCurl from "fetch-to-curl";
import { Link, NavLink, Outlet, useParams } from "react-router-dom";
import { Link, Outlet, useParams } from "react-router-dom";
import { statusIsFinalized, statusIsRunningOrQueued } from "../tasks/types";
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
import { useWorkflowRunQuery } from "./hooks/useWorkflowRunQuery";
import { cn } from "@/util/utils";
import { ProxyLocation } from "@/api/types";
function WorkflowRun() {
const { workflowRunId, workflowPermanentId } = useParams();
@@ -210,64 +210,26 @@ function WorkflowRun() {
</div>
</header>
{workflowFailureReason}
<div className="flex w-fit gap-2 rounded-sm border border-slate-700 p-2">
<NavLink
to="blocks"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Blocks
</NavLink>
<NavLink
to="output"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Output
</NavLink>
<NavLink
to="parameters"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Parameters
</NavLink>
<NavLink
to="recording"
replace
className={({ isActive }) => {
return cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": isActive,
},
);
}}
>
Recording
</NavLink>
</div>
<SwitchBarNavigation
options={[
{
label: "Overview",
to: "overview",
},
{
label: "Output",
to: "output",
},
{
label: "Parameters",
to: "parameters",
},
{
label: "Recording",
to: "recording",
},
]}
/>
<Outlet />
</div>
);

View File

@@ -396,14 +396,14 @@ function Workflows() {
if (event.ctrlKey || event.metaKey) {
window.open(
window.location.origin +
`/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}/blocks`,
`/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}/overview`,
"_blank",
"noopener,noreferrer",
);
return;
}
navigate(
`/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}/blocks`,
`/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}/overview`,
);
}}
className="cursor-pointer"

View File

@@ -19,10 +19,11 @@ import { Checkbox } from "@/components/ui/checkbox";
import { errorMappingExampleValue } from "../types";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { Switch } from "@/components/ui/switch";
import { ClickIcon } from "@/components/icons/ClickIcon";
import { placeholders, helpTooltips } from "../../helpContent";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
const urlTooltip =
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
@@ -78,7 +79,10 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
<header className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<ClickIcon className="size-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Action}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -2,11 +2,12 @@ import { Label } from "@/components/ui/label";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { CodeIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { useState } from "react";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import type { CodeBlockNode } from "./types";
function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
@@ -38,7 +39,10 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<CodeIcon className="h-6 w-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Code}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -1,14 +1,15 @@
import { HelpTooltip } from "@/components/HelpTooltip";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { DownloadIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position } from "@xyflow/react";
import { helpTooltips } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import type { DownloadNode } from "./types";
import { HelpTooltip } from "@/components/HelpTooltip";
import { helpTooltips } from "../../helpContent";
function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
const [label, setLabel] = useNodeLabelChangeHandler({
@@ -35,7 +36,10 @@ function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<DownloadIcon className="h-6 w-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.DownloadToS3}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -1,15 +1,16 @@
import { HelpTooltip } from "@/components/HelpTooltip";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { CursorTextIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { useState } from "react";
import { helpTooltips } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { type FileParserNode } from "./types";
import { Label } from "@/components/ui/label";
import { HelpTooltip } from "@/components/HelpTooltip";
import { helpTooltips } from "../../helpContent";
function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
const { updateNodeData } = useReactFlow();
@@ -40,7 +41,10 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<CursorTextIcon className="h-6 w-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.FileURLParser}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -14,13 +14,14 @@ import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTexta
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { LockOpen1Icon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { useState } from "react";
import { helpTooltips, placeholders } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { errorMappingExampleValue } from "../types";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { CredentialParameterSelector } from "./CredentialParameterSelector";
import type { LoginNode } from "./types";
@@ -72,7 +73,10 @@ function LoginNode({ id, data }: NodeProps<LoginNode>) {
<header className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<LockOpen1Icon className="size-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Login}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -1,3 +1,4 @@
import { HelpTooltip } from "@/components/HelpTooltip";
import { Label } from "@/components/ui/label";
import {
Select,
@@ -8,7 +9,7 @@ import {
} from "@/components/ui/select";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { UpdateIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import type { Node } from "@xyflow/react";
import {
Handle,
@@ -19,13 +20,13 @@ import {
useReactFlow,
} from "@xyflow/react";
import { AppNode } from "..";
import { helpTooltips } from "../../helpContent";
import { useWorkflowParametersState } from "../../useWorkflowParametersState";
import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import type { LoopNode } from "./types";
import { HelpTooltip } from "@/components/HelpTooltip";
import { helpTooltips } from "../../helpContent";
function LoopNode({ id, data }: NodeProps<LoopNode>) {
const { updateNodeData } = useReactFlow();
@@ -88,7 +89,10 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<UpdateIcon className="h-6 w-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.ForLoop}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -1,28 +1,29 @@
import { HelpTooltip } from "@/components/HelpTooltip";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { useState } from "react";
import { helpTooltips, placeholders } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { HelpTooltip } from "@/components/HelpTooltip";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { errorMappingExampleValue } from "../types";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { Switch } from "@/components/ui/switch";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import type { NavigationNode } from "./types";
import { RobotIcon } from "@/components/icons/RobotIcon";
import { helpTooltips, placeholders } from "../../helpContent";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
function NavigationNode({ id, data }: NodeProps<NavigationNode>) {
const { updateNodeData } = useReactFlow();
@@ -74,7 +75,10 @@ function NavigationNode({ id, data }: NodeProps<NavigationNode>) {
<header className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<RobotIcon className="size-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Navigation}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -4,12 +4,13 @@ import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { EnvelopeClosedIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { useState } from "react";
import { helpTooltips } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { type SendEmailNode } from "./types";
function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
@@ -52,7 +53,10 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<EnvelopeClosedIcon className="h-6 w-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.SendEmail}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -10,10 +10,12 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { ListBulletIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import {
Handle,
NodeProps,
@@ -29,10 +31,9 @@ import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { dataSchemaExampleValue, errorMappingExampleValue } from "../types";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { ParametersMultiSelect } from "./ParametersMultiSelect";
import type { TaskNode } from "./types";
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
function TaskNode({ id, data }: NodeProps<TaskNode>) {
const { updateNodeData } = useReactFlow();
@@ -91,7 +92,10 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<ListBulletIcon className="h-6 w-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Task}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -6,7 +6,7 @@ import { Separator } from "@/components/ui/separator";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { CursorTextIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import {
Handle,
NodeProps,
@@ -22,6 +22,7 @@ import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { type TextPromptNode } from "./types";
function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
@@ -60,7 +61,10 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<CursorTextIcon className="h-6 w-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.TextPrompt}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -3,11 +3,12 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { UploadIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position } from "@xyflow/react";
import { helpTooltips } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import { type UploadNode } from "./types";
function UploadNode({ id, data }: NodeProps<UploadNode>) {
@@ -35,7 +36,10 @@ function UploadNode({ id, data }: NodeProps<UploadNode>) {
<div className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<UploadIcon className="h-6 w-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.UploadToS3}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -1,26 +1,27 @@
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import type { ValidationNode } from "./types";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { useState } from "react";
import { CheckCircledIcon } from "@radix-ui/react-icons";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { Label } from "@/components/ui/label";
import { HelpTooltip } from "@/components/HelpTooltip";
import { Checkbox } from "@/components/ui/checkbox";
import { errorMappingExampleValue } from "../types";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { Switch } from "@/components/ui/switch";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { helpTooltips } from "../../helpContent";
import { Switch } from "@/components/ui/switch";
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { useState } from "react";
import { helpTooltips } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { errorMappingExampleValue } from "../types";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import type { ValidationNode } from "./types";
function ValidationNode({ id, data }: NodeProps<ValidationNode>) {
const { updateNodeData } = useReactFlow();
@@ -62,7 +63,10 @@ function ValidationNode({ id, data }: NodeProps<ValidationNode>) {
<header className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<CheckCircledIcon className="size-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Validation}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -1,15 +1,16 @@
import { HelpTooltip } from "@/components/HelpTooltip";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { StopwatchIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
import { useState } from "react";
import { helpTooltips } from "../../helpContent";
import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu";
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
import type { WaitNode } from "./types";
import { HelpTooltip } from "@/components/HelpTooltip";
import { helpTooltips } from "../../helpContent";
function WaitNode({ id, data }: NodeProps<WaitNode>) {
const { updateNodeData } = useReactFlow();
@@ -49,7 +50,10 @@ function WaitNode({ id, data }: NodeProps<WaitNode>) {
<header className="flex h-[2.75rem] justify-between">
<div className="flex gap-2">
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
<StopwatchIcon className="size-6" />
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Wait}
className="size-6"
/>
</div>
<div className="flex flex-col gap-1">
<EditableNodeTitle

View File

@@ -0,0 +1,73 @@
import { ClickIcon } from "@/components/icons/ClickIcon";
import { WorkflowBlockType } from "../../types/workflowTypes";
import {
CheckCircledIcon,
CodeIcon,
CursorTextIcon,
DownloadIcon,
EnvelopeClosedIcon,
ListBulletIcon,
LockOpen1Icon,
StopwatchIcon,
UpdateIcon,
UploadIcon,
} from "@radix-ui/react-icons";
import { ExtractIcon } from "@/components/icons/ExtractIcon";
import { RobotIcon } from "@/components/icons/RobotIcon";
type Props = {
workflowBlockType: WorkflowBlockType;
className?: string;
};
function WorkflowBlockIcon({ workflowBlockType, className }: Props) {
switch (workflowBlockType) {
case "action": {
return <ClickIcon className={className} />;
}
case "code": {
return <CodeIcon className={className} />;
}
case "download_to_s3": {
return <DownloadIcon className={className} />;
}
case "extraction": {
return <ExtractIcon className={className} />;
}
case "file_download": {
return <DownloadIcon className={className} />;
}
case "file_url_parser": {
return <CursorTextIcon className={className} />;
}
case "for_loop": {
return <UpdateIcon className={className} />;
}
case "login": {
return <LockOpen1Icon className={className} />;
}
case "navigation": {
return <RobotIcon className={className} />;
}
case "send_email": {
return <EnvelopeClosedIcon className={className} />;
}
case "task": {
return <ListBulletIcon className={className} />;
}
case "text_prompt": {
return <CursorTextIcon className={className} />;
}
case "upload_to_s3": {
return <UploadIcon className={className} />;
}
case "validation": {
return <CheckCircledIcon className={className} />;
}
case "wait": {
return <StopwatchIcon className={className} />;
}
}
}
export { WorkflowBlockIcon };

View File

@@ -1,3 +1,5 @@
import { WorkflowBlockType } from "../../types/workflowTypes";
export type NodeBaseData = {
label: string;
continueOnFailure: boolean;
@@ -14,3 +16,23 @@ export const dataSchemaExampleValue = {
sample: { type: "string" },
},
} as const;
export const workflowBlockTitle: {
[blockType in WorkflowBlockType]: string;
} = {
action: "Action",
code: "Code",
download_to_s3: "Download",
extraction: "Extraction",
file_download: "File Download",
file_url_parser: "File Parser",
for_loop: "Loop",
login: "Login",
navigation: "Navigation",
send_email: "Send Email",
task: "Task",
text_prompt: "Text Prompt",
upload_to_s3: "Upload",
validation: "Validation",
wait: "Wait",
};

View File

@@ -1,24 +1,10 @@
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
import {
CheckCircledIcon,
Cross2Icon,
CursorTextIcon,
DownloadIcon,
EnvelopeClosedIcon,
FileIcon,
ListBulletIcon,
LockOpen1Icon,
PlusIcon,
StopwatchIcon,
UpdateIcon,
UploadIcon,
} from "@radix-ui/react-icons";
import { WorkflowBlockNode } from "../nodes";
import { AddNodeProps } from "../FlowRenderer";
import { ClickIcon } from "@/components/icons/ClickIcon";
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
import { RobotIcon } from "@/components/icons/RobotIcon";
import { ExtractIcon } from "@/components/icons/ExtractIcon";
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
import { WorkflowBlockTypes } from "../../types/workflowTypes";
import { AddNodeProps } from "../FlowRenderer";
import { WorkflowBlockNode } from "../nodes";
import { WorkflowBlockIcon } from "../nodes/WorkflowBlockIcon";
const nodeLibraryItems: Array<{
nodeType: NonNullable<WorkflowBlockNode["type"]>;
@@ -28,93 +14,166 @@ const nodeLibraryItems: Array<{
}> = [
{
nodeType: "navigation",
icon: <RobotIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Navigation}
className="size-6"
/>
),
title: "Navigation Block",
description: "Navigate on the page",
},
{
nodeType: "action",
icon: <ClickIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Action}
className="size-6"
/>
),
title: "Action Block",
description: "Take a single action",
},
{
nodeType: "extraction",
icon: <ExtractIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Extraction}
className="size-6"
/>
),
title: "Extraction Block",
description: "Extract data from the page",
},
{
nodeType: "validation",
icon: <CheckCircledIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Validation}
className="size-6"
/>
),
title: "Validation Block",
description: "Validate the state of the workflow or terminate",
},
{
nodeType: "task",
icon: <ListBulletIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Task}
className="size-6"
/>
),
title: "Task Block",
description: "Takes actions or extracts information",
},
{
nodeType: "textPrompt",
icon: <CursorTextIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.TextPrompt}
className="size-6"
/>
),
title: "Text Prompt Block",
description: "Generates AI response",
},
{
nodeType: "sendEmail",
icon: <EnvelopeClosedIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.SendEmail}
className="size-6"
/>
),
title: "Send Email Block",
description: "Sends an email",
},
{
nodeType: "loop",
icon: <UpdateIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.ForLoop}
className="size-6"
/>
),
title: "For Loop Block",
description: "Repeats nested elements",
},
// temporarily removed
// {
// nodeType: "codeBlock",
// icon: <CodeIcon className="size-6" />,
// icon: <WorkflowBlockIcon
// workflowBlockType={WorkflowBlockTypes.Code}
// className="size-6"
// />,
// title: "Code Block",
// description: "Executes Python code",
// },
{
nodeType: "fileParser",
icon: <FileIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.FileURLParser}
className="size-6"
/>
),
title: "File Parser Block",
description: "Downloads and parses a file",
},
// disabled
// {
// nodeType: "download",
// icon: <DownloadIcon className="size-6" />,
// icon: (
// <WorkflowBlockIcon
// workflowBlockType={WorkflowBlockTypes.DownloadToS3}
// className="size-6"
// />
// ),
// title: "Download Block",
// description: "Downloads a file from S3",
// },
{
nodeType: "upload",
icon: <UploadIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.UploadToS3}
className="size-6"
/>
),
title: "Upload Block",
description: "Uploads a file to S3",
},
{
nodeType: "fileDownload",
icon: <DownloadIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.FileDownload}
className="size-6"
/>
),
title: "File Download Block",
description: "Download a file",
},
{
nodeType: "login",
icon: <LockOpen1Icon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Login}
className="size-6"
/>
),
title: "Login Block",
description: "Login to a website",
},
{
nodeType: "wait",
icon: <StopwatchIcon className="size-6" />,
icon: (
<WorkflowBlockIcon
workflowBlockType={WorkflowBlockTypes.Wait}
className="size-6"
/>
),
title: "Wait Block",
description: "Wait for some time",
},

View File

@@ -0,0 +1,32 @@
import { getClient } from "@/api/AxiosClient";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { statusIsNotFinalized } from "@/routes/tasks/types";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { WorkflowRunTimelineItem } from "../types/workflowRunTypes";
import { useWorkflowRunQuery } from "./useWorkflowRunQuery";
function useWorkflowRunTimelineQuery() {
const { workflowRunId, workflowPermanentId } = useParams();
const credentialGetter = useCredentialGetter();
const { data: workflowRun } = useWorkflowRunQuery();
return useQuery<Array<WorkflowRunTimelineItem>>({
queryKey: ["workflowRunTimeline", workflowPermanentId, workflowRunId],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client
.get(`/workflows/${workflowPermanentId}/runs/${workflowRunId}/timeline`)
.then((response) => response.data);
},
refetchInterval:
workflowRun && statusIsNotFinalized(workflowRun) ? 5000 : false,
placeholderData: keepPreviousData,
refetchOnMount:
workflowRun && statusIsNotFinalized(workflowRun) ? "always" : false,
refetchOnWindowFocus:
workflowRun && statusIsNotFinalized(workflowRun) ? "always" : false,
});
}
export { useWorkflowRunTimelineQuery };

View File

@@ -0,0 +1,138 @@
import { ActionsApiResponse, Status } from "@/api/types";
import { isTaskVariantBlock, WorkflowBlockType } from "./workflowTypes";
export const WorkflowRunTimelineItemTypes = {
Thought: "thought",
Block: "block",
} as const;
export type WorkflowRunTimelineItemType =
(typeof WorkflowRunTimelineItemTypes)[keyof typeof WorkflowRunTimelineItemTypes];
export type ObserverThought = {
observer_thought_id: string;
user_input: string | null;
observation: string | null;
thought: string | null;
answer: string | null;
created_at: string;
modified_at: string;
};
export type WorkflowRunBlock = {
workflow_run_block_id: string;
workflow_run_id: string;
parent_workflow_run_block_id: string | null;
block_type: WorkflowBlockType;
label: string | null;
title: string | null;
status: Status | null;
failure_reason: string | null;
output: object | Array<unknown> | string | null;
continue_on_failure: boolean;
task_id: string | null;
url: string | null;
navigation_goal: string | null;
navigation_payload: Record<string, unknown> | null;
data_extraction_goal: string | null;
data_schema: object | Array<unknown> | string | null;
terminate_criterion: string | null;
complete_criterion: string | null;
actions: Array<ActionsApiResponse> | null;
recipients?: Array<string> | null;
attachments?: Array<string> | null;
subject?: string | null;
body?: string | null;
prompt?: string | null;
wait_sec?: number | null;
created_at: string;
modified_at: string;
};
export type WorkflowRunTimelineBlockItem = {
type: "block";
block: WorkflowRunBlock;
children: Array<WorkflowRunTimelineItem>;
thought: null;
created_at: string;
modified_at: string;
};
export type WorkflowRunTimelineThoughtItem = {
type: "thought";
block: null;
children: Array<WorkflowRunTimelineItem>;
thought: ObserverThought;
created_at: string;
modified_at: string;
};
export type WorkflowRunTimelineItem =
| WorkflowRunTimelineBlockItem
| WorkflowRunTimelineThoughtItem;
export function isThoughtItem(
item: unknown,
): item is WorkflowRunTimelineThoughtItem {
return (
typeof item === "object" &&
item !== null &&
"type" in item &&
item.type === "thought" &&
"thought" in item &&
item.thought !== null
);
}
export function isBlockItem(
item: unknown,
): item is WorkflowRunTimelineBlockItem {
return (
typeof item === "object" &&
item !== null &&
"type" in item &&
item.type === "block" &&
"block" in item &&
item.block !== null
);
}
export function isTaskVariantBlockItem(item: unknown) {
return isBlockItem(item) && isTaskVariantBlock(item.block);
}
export function isWorkflowRunBlock(item: unknown): item is WorkflowRunBlock {
return (
typeof item === "object" &&
item !== null &&
"block_type" in item &&
"workflow_run_block_id" in item
);
}
export function isObserverThought(item: unknown): item is ObserverThought {
return (
typeof item === "object" &&
item !== null &&
"observer_thought_id" in item &&
"thought" in item
);
}
export function isAction(item: unknown): item is ActionsApiResponse {
return typeof item === "object" && item !== null && "action_id" in item;
}
export function hasExtractedInformation(
item: unknown,
): item is { extracted_information: unknown } {
return (
item !== null && typeof item === "object" && "extracted_information" in item
);
}
export function hasNavigationGoal(
item: unknown,
): item is { navigation_goal: unknown } {
return item !== null && typeof item === "object" && "navigation_goal" in item;
}

View File

@@ -120,7 +120,7 @@ export type WorkflowBlock =
| WaitBlock
| FileDownloadBlock;
export const WorkflowBlockType = {
export const WorkflowBlockTypes = {
Task: "task",
ForLoop: "for_loop",
Code: "code",
@@ -138,8 +138,22 @@ export const WorkflowBlockType = {
FileDownload: "file_download",
} as const;
export function isTaskVariantBlock(item: {
block_type: WorkflowBlockType;
}): boolean {
return (
item.block_type === "task" ||
item.block_type === "navigation" ||
item.block_type === "action" ||
item.block_type === "extraction" ||
item.block_type === "validation" ||
item.block_type === "login" ||
item.block_type === "file_download"
);
}
export type WorkflowBlockType =
(typeof WorkflowBlockType)[keyof typeof WorkflowBlockType];
(typeof WorkflowBlockTypes)[keyof typeof WorkflowBlockTypes];
export type WorkflowBlockBase = {
label: string;

View File

@@ -1,3 +1,5 @@
import { WorkflowBlockType } from "./workflowTypes";
export type WorkflowCreateYAMLRequest = {
title: string;
description?: string | null;
@@ -67,26 +69,6 @@ export type OutputParameterYAML = ParameterYAMLBase & {
parameter_type: "output";
};
const BlockTypes = {
TASK: "task",
FOR_LOOP: "for_loop",
CODE: "code",
TEXT_PROMPT: "text_prompt",
DOWNLOAD_TO_S3: "download_to_s3",
UPLOAD_TO_S3: "upload_to_s3",
SEND_EMAIL: "send_email",
FILE_URL_PARSER: "file_url_parser",
VALIDATION: "validation",
ACTION: "action",
NAVIGATION: "navigation",
EXTRACTION: "extraction",
LOGIN: "login",
WAIT: "wait",
FILE_DOWNLOAD: "file_download",
} as const;
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
export type BlockYAML =
| TaskBlockYAML
| CodeBlockYAML
@@ -105,7 +87,7 @@ export type BlockYAML =
| FileDownloadBlockYAML;
export type BlockYAMLBase = {
block_type: BlockType;
block_type: WorkflowBlockType;
label: string;
continue_on_failure?: boolean;
};

View File

@@ -0,0 +1,63 @@
import { ActionsApiResponse, ActionTypes, Status } from "@/api/types";
import { Separator } from "@/components/ui/separator";
import { ActionTypePill } from "@/routes/tasks/detail/ActionTypePill";
import { cn } from "@/util/utils";
import { CheckCircledIcon, CrossCircledIcon } from "@radix-ui/react-icons";
type Props = {
action: ActionsApiResponse;
index: number;
active: boolean;
onClick: () => void;
};
function ActionCard({ action, onClick, active, index }: Props) {
const success = action.status === Status.Completed;
return (
<div
className={cn(
"flex cursor-pointer rounded-lg border-2 bg-slate-elevation3 hover:border-slate-50",
{
"border-l-destructive": !success,
"border-l-success": success,
"border-slate-50": active,
},
)}
onClick={onClick}
>
<div className="flex-1 space-y-2 p-4 pl-5">
<div className="flex justify-between">
<div className="flex items-center gap-2">
<span>#{index}</span>
</div>
<div className="flex items-center gap-2">
<ActionTypePill actionType={action.action_type} />
{success ? (
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
<CheckCircledIcon className="h-4 w-4 text-success" />
<span className="text-xs">Success</span>
</div>
) : (
<div className="flex gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
<CrossCircledIcon className="h-4 w-4 text-destructive" />
<span className="text-xs">Fail</span>
</div>
)}
</div>
</div>
<div className="text-xs text-slate-400">{action.reasoning}</div>
{action.action_type === ActionTypes.InputText && (
<>
<Separator />
<div className="text-xs text-slate-400">
Input: {action.response}
</div>
</>
)}
</div>
</div>
);
}
export { ActionCard };

View File

@@ -0,0 +1,41 @@
import { CubeIcon } from "@radix-ui/react-icons";
import { WorkflowRunBlock } from "../types/workflowRunTypes";
import { cn } from "@/util/utils";
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
import { workflowBlockTitle } from "../editor/nodes/types";
type Props = {
active: boolean;
block: WorkflowRunBlock;
onClick: () => void;
};
function BlockCard({ block, onClick, active }: Props) {
return (
<div
className={cn(
"cursor-pointer space-y-3 rounded-md border bg-slate-elevation3 p-4 hover:border-slate-50",
{
"border-slate-50": active,
},
)}
onClick={onClick}
>
<div className="flex justify-between">
<div className="flex gap-3">
<WorkflowBlockIcon
workflowBlockType={block.block_type}
className="size-6"
/>
<span>{workflowBlockTitle[block.block_type]}</span>
</div>
<div className="flex items-center gap-1 rounded bg-slate-elevation5 px-2 py-1">
<CubeIcon className="size-4" />
<span className="text-xs">Block</span>
</div>
</div>
</div>
);
}
export { BlockCard };

View File

@@ -0,0 +1,76 @@
import { getClient } from "@/api/AxiosClient";
import { ArtifactApiResponse, ArtifactType, Status } from "@/api/types";
import { ZoomableImage } from "@/components/ZoomableImage";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useQuery } from "@tanstack/react-query";
import { ReloadIcon } from "@radix-ui/react-icons";
import { statusIsNotFinalized } from "@/routes/tasks/types";
import { getImageURL } from "@/routes/tasks/detail/artifactUtils";
type Props = {
observerThoughtId: string;
taskStatus?: Status; // to give a hint that screenshot may not be available if task is not finalized
};
function ObserverThoughtScreenshot({ observerThoughtId, taskStatus }: Props) {
const credentialGetter = useCredentialGetter();
const { data: artifacts, isLoading } = useQuery<Array<ArtifactApiResponse>>({
queryKey: ["observerThought", observerThoughtId, "artifacts"],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client
.get(`/observer_thought/${observerThoughtId}/artifacts`)
.then((response) => response.data);
},
refetchInterval: (query) => {
const data = query.state.data;
const screenshot = data?.filter(
(artifact) => artifact.artifact_type === ArtifactType.LLMScreenshot,
)?.[0];
if (!screenshot) {
return 5000;
}
return false;
},
});
const llmScreenshots = artifacts?.filter(
(artifact) => artifact.artifact_type === ArtifactType.LLMScreenshot,
);
const screenshot = llmScreenshots?.[0];
if (isLoading) {
return (
<div className="flex h-full items-center justify-center gap-2 bg-slate-elevation1">
<ReloadIcon className="h-6 w-6 animate-spin" />
<div>Loading screenshot...</div>
</div>
);
}
if (
!screenshot &&
taskStatus &&
statusIsNotFinalized({ status: taskStatus })
) {
return <div>The screenshot for this action is not available yet.</div>;
}
if (!screenshot) {
return (
<div className="flex h-full items-center justify-center bg-slate-elevation1">
No screenshot found for this action.
</div>
);
}
return (
<figure className="mx-auto flex max-w-full flex-col items-center gap-2 overflow-hidden rounded">
<ZoomableImage src={getImageURL(screenshot)} alt="llm-screenshot" />
</figure>
);
}
export { ObserverThoughtScreenshot };

View File

@@ -0,0 +1,36 @@
import { PersonIcon } from "@radix-ui/react-icons";
import { ObserverThought } from "../types/workflowRunTypes";
import { cn } from "@/util/utils";
type Props = {
active: boolean;
thought: ObserverThought;
onClick: (thought: ObserverThought) => void;
};
function ThoughtCard({ thought, onClick, active }: Props) {
return (
<div
className={cn(
"space-y-3 rounded-md border bg-slate-elevation3 p-4 hover:border-slate-50",
{
"border-slate-50": active,
},
)}
onClick={() => {
onClick(thought);
}}
>
<div className="flex justify-between">
<span>Thought</span>
<div className="flex items-center gap-1 bg-slate-elevation5">
<PersonIcon className="size-4" />
<span className="text-xs">Decision</span>
</div>
</div>
<div className="text-xs text-slate-400">{thought.answer}</div>
</div>
);
}
export { ThoughtCard };

View File

@@ -0,0 +1,171 @@
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
import { WorkflowRunOverviewSkeleton } from "./WorkflowRunOverviewSkeleton";
import { useState } from "react";
import { statusIsNotFinalized } from "@/routes/tasks/types";
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
import { WorkflowRunStream } from "./WorkflowRunStream";
import { ActionScreenshot } from "@/routes/tasks/detail/ActionScreenshot";
import { WorkflowRunTimelineBlockItem } from "./WorkflowRunTimelineBlockItem";
import { ThoughtCard } from "./ThoughtCard";
import {
isAction,
isBlockItem,
isObserverThought,
isThoughtItem,
isWorkflowRunBlock,
ObserverThought,
WorkflowRunBlock,
} from "../types/workflowRunTypes";
import { ActionsApiResponse } from "@/api/types";
import { cn } from "@/util/utils";
import { DotFilledIcon } from "@radix-ui/react-icons";
import { WorkflowRunTimelineItemInfoSection } from "./WorkflowRunTimelineItemInfoSection";
import { ObserverThoughtScreenshot } from "./ObserverThoughtScreenshot";
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
export type WorkflowRunOverviewActiveElement =
| ActionsApiResponse
| ObserverThought
| WorkflowRunBlock
| "stream"
| null;
function WorkflowRunOverview() {
const [active, setActive] = useState<WorkflowRunOverviewActiveElement>(null);
const { data: workflowRun, isLoading: workflowRunIsLoading } =
useWorkflowRunQuery();
const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } =
useWorkflowRunTimelineQuery();
if (workflowRunIsLoading || workflowRunTimelineIsLoading) {
return <WorkflowRunOverviewSkeleton />;
}
if (!workflowRun) {
return null;
}
if (typeof workflowRunTimeline === "undefined") {
return null;
}
const workflowRunIsNotFinalized = statusIsNotFinalized(workflowRun);
const timeline = workflowRunTimeline.slice().reverse();
function getActiveSelection(): WorkflowRunOverviewActiveElement {
if (active === null) {
if (workflowRunIsNotFinalized) {
return "stream";
}
if (timeline!.length > 0) {
const timelineItem = timeline![0];
if (isBlockItem(timelineItem)) {
if (
timelineItem.block.actions &&
timelineItem.block.actions.length > 0
) {
return timelineItem.block
.actions[0] as WorkflowRunOverviewActiveElement;
}
return timelineItem.block;
}
if (isThoughtItem(timelineItem)) {
return timelineItem.thought;
}
}
}
return active;
}
const selection = getActiveSelection();
return (
<div className="flex h-[42rem] gap-6">
<div className="w-2/3 space-y-4">
<AspectRatio ratio={16 / 9} className="overflow-y-hidden">
{selection === "stream" && <WorkflowRunStream />}
{selection !== "stream" && isAction(selection) && (
<ActionScreenshot
index={selection.action_order ?? 0}
stepId={selection.step_id ?? ""}
/>
)}
{isWorkflowRunBlock(selection) && (
<div className="flex h-full w-full items-center justify-center bg-slate-elevation1">
No screenshot found for this block
</div>
)}
{isObserverThought(selection) && (
<ObserverThoughtScreenshot
observerThoughtId={selection.observer_thought_id}
/>
)}
</AspectRatio>
<WorkflowRunTimelineItemInfoSection item={selection} />
</div>
<div className="w-1/3 min-w-0 rounded bg-slate-elevation1 p-4">
<ScrollArea>
<ScrollAreaViewport className="max-h-[42rem]">
<div className="space-y-4">
<div className="gap-2"></div>
{workflowRunIsNotFinalized && (
<div
key="stream"
className={cn(
"flex cursor-pointer rounded-lg border-2 bg-slate-elevation3 p-4 hover:border-slate-50",
{
"border-slate-50": selection === "stream",
},
)}
onClick={() => setActive("stream")}
>
<div className="flex items-center gap-2">
<DotFilledIcon className="h-6 w-6 text-destructive" />
Live
</div>
</div>
)}
{timeline.length === 0 && <div>Workflow timeline is empty</div>}
{timeline?.map((timelineItem) => {
if (isBlockItem(timelineItem)) {
return (
<WorkflowRunTimelineBlockItem
key={timelineItem.block.workflow_run_block_id}
subBlocks={timelineItem.children
.filter((item) => item.type === "block")
.map((item) => item.block)}
activeItem={selection}
block={timelineItem.block}
onActionClick={setActive}
onBlockItemClick={setActive}
/>
);
}
if (isThoughtItem(timelineItem)) {
return (
<ThoughtCard
key={timelineItem.thought.observer_thought_id}
active={
isObserverThought(selection) &&
selection.observer_thought_id ===
timelineItem.thought.observer_thought_id
}
onClick={setActive}
thought={timelineItem.thought}
/>
);
}
})}
</div>
</ScrollAreaViewport>
</ScrollArea>
</div>
</div>
);
}
export { WorkflowRunOverview };

View File

@@ -0,0 +1,22 @@
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Skeleton } from "@/components/ui/skeleton";
function WorkflowRunOverviewSkeleton() {
return (
<div className="flex h-[42rem] gap-6">
<div className="w-2/3 space-y-4">
<AspectRatio ratio={16 / 9}>
<Skeleton className="h-full w-full" />
</AspectRatio>
<div className="h-[10rem]">
<Skeleton className="h-full w-full" />
</div>
</div>
<div className="w-1/3">
<Skeleton className="h-full w-full" />
</div>
</div>
);
}
export { WorkflowRunOverviewSkeleton };

View File

@@ -0,0 +1,152 @@
import { Status } from "@/api/types";
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
import { ZoomableImage } from "@/components/ZoomableImage";
import { useEffect, useState } from "react";
import { statusIsNotFinalized } from "@/routes/tasks/types";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useParams } from "react-router-dom";
import { envCredential } from "@/util/env";
import { toast } from "@/components/ui/use-toast";
import { useQueryClient } from "@tanstack/react-query";
type StreamMessage = {
task_id: string;
status: string;
screenshot?: string;
};
let socket: WebSocket | null = null;
const wssBaseUrl = import.meta.env.VITE_WSS_BASE_URL;
function WorkflowRunStream() {
const { data: workflowRun } = useWorkflowRunQuery();
const [streamImgSrc, setStreamImgSrc] = useState<string>("");
const showStream = workflowRun && statusIsNotFinalized(workflowRun);
const credentialGetter = useCredentialGetter();
const { workflowRunId, workflowPermanentId } = useParams();
const queryClient = useQueryClient();
useEffect(() => {
if (!showStream) {
return;
}
async function run() {
// Create WebSocket connection.
let credential = null;
if (credentialGetter) {
const token = await credentialGetter();
credential = `?token=Bearer ${token}`;
} else {
credential = `?apikey=${envCredential}`;
}
if (socket) {
socket.close();
}
socket = new WebSocket(
`${wssBaseUrl}/stream/workflow_runs/${workflowRunId}${credential}`,
);
// Listen for messages
socket.addEventListener("message", (event) => {
try {
const message: StreamMessage = JSON.parse(event.data);
if (message.screenshot) {
setStreamImgSrc(message.screenshot);
}
if (
message.status === "completed" ||
message.status === "failed" ||
message.status === "terminated"
) {
socket?.close();
queryClient.invalidateQueries({
queryKey: ["workflowRuns"],
});
queryClient.invalidateQueries({
queryKey: ["workflowRun", workflowPermanentId, workflowRunId],
});
queryClient.invalidateQueries({
queryKey: ["workflowTasks", workflowRunId],
});
if (
message.status === "failed" ||
message.status === "terminated"
) {
toast({
title: "Run Failed",
description: "The workflow run has failed.",
variant: "destructive",
});
} else if (message.status === "completed") {
toast({
title: "Run Completed",
description: "The workflow run has been completed.",
variant: "success",
});
}
}
} catch (e) {
console.error("Failed to parse message", e);
}
});
socket.addEventListener("close", () => {
socket = null;
});
}
run();
return () => {
if (socket) {
socket.close();
socket = null;
}
};
}, [
credentialGetter,
workflowRunId,
showStream,
queryClient,
workflowPermanentId,
]);
if (workflowRun?.status === Status.Created) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-8 rounded-md bg-slate-900 py-8 text-lg">
<span>Workflow has been created.</span>
<span>Stream will start when the workflow is running.</span>
</div>
);
}
if (workflowRun?.status === Status.Queued) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-8 rounded-md bg-slate-900 py-8 text-lg">
<span>Your workflow run is queued.</span>
<span>Stream will start when the workflow is running.</span>
</div>
);
}
if (workflowRun?.status === Status.Running && streamImgSrc.length === 0) {
return (
<div className="flex h-full w-full items-center justify-center rounded-md bg-slate-900 py-8 text-lg">
Starting the stream...
</div>
);
}
if (workflowRun?.status === Status.Running && streamImgSrc.length > 0) {
return (
<div className="h-full w-full">
<ZoomableImage
src={`data:image/png;base64,${streamImgSrc}`}
className="rounded-md"
/>
</div>
);
}
return null;
}
export { WorkflowRunStream };

View File

@@ -0,0 +1,70 @@
import { ActionsApiResponse } from "@/api/types";
import {
isAction,
isWorkflowRunBlock,
WorkflowRunBlock,
} from "../types/workflowRunTypes";
import { ActionCard } from "./ActionCard";
import { BlockCard } from "./BlockCard";
import { WorkflowRunOverviewActiveElement } from "./WorkflowRunOverview";
type Props = {
activeItem: WorkflowRunOverviewActiveElement;
block: WorkflowRunBlock;
subBlocks: Array<WorkflowRunBlock>;
onBlockItemClick: (block: WorkflowRunBlock) => void;
onActionClick: (action: ActionsApiResponse) => void;
};
function WorkflowRunTimelineBlockItem({
activeItem,
block,
subBlocks,
onBlockItemClick,
onActionClick,
}: Props) {
const actions = block.actions ? [...block.actions].reverse() : [];
return (
<div className="space-y-4 rounded border border-slate-600 p-4">
{actions.map((action, index) => {
return (
<ActionCard
key={action.action_id}
action={action}
active={
isAction(activeItem) && activeItem.action_id === action.action_id
}
index={actions.length - index}
onClick={() => {
onActionClick(action);
}}
/>
);
})}
{subBlocks.map((block) => {
return (
<WorkflowRunTimelineBlockItem
block={block}
activeItem={activeItem}
onActionClick={onActionClick}
onBlockItemClick={onBlockItemClick}
subBlocks={[]}
/>
);
})}
<BlockCard
active={
isWorkflowRunBlock(activeItem) &&
activeItem.workflow_run_block_id === block.workflow_run_block_id
}
block={block}
onClick={() => {
onBlockItemClick(block);
}}
/>
</div>
);
}
export { WorkflowRunTimelineBlockItem };

View File

@@ -0,0 +1,211 @@
import { ActionsApiResponse, Status } from "@/api/types";
import {
hasExtractedInformation,
isAction,
isObserverThought,
isWorkflowRunBlock,
ObserverThought,
WorkflowRunBlock,
} from "../types/workflowRunTypes";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CodeEditor } from "../components/CodeEditor";
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
import { WorkflowBlockTypes } from "../types/workflowTypes";
import { statusIsAFailureType } from "@/routes/tasks/types";
import { SendEmailBlockInfo } from "./blockInfo/SendEmailBlockInfo";
type Props = {
item:
| ActionsApiResponse
| ObserverThought
| WorkflowRunBlock
| "stream"
| null;
};
function WorkflowRunTimelineItemInfoSection({ item }: Props) {
if (!item) {
return null;
}
if (item === "stream") {
return null;
}
if (isAction(item)) {
return null;
}
if (isObserverThought(item)) {
return (
<div className="rounded bg-slate-elevation1 p-4">
<Tabs key="thought" defaultValue="observation">
<TabsList>
<TabsTrigger value="observation">Observation</TabsTrigger>
<TabsTrigger value="thought">Thought</TabsTrigger>
<TabsTrigger value="answer">Answer</TabsTrigger>
</TabsList>
<TabsContent value="observation">
<AutoResizingTextarea value={item.observation ?? ""} readOnly />
</TabsContent>
<TabsContent value="thought">
<AutoResizingTextarea value={item.thought ?? ""} readOnly />
</TabsContent>
<TabsContent value="answer">
<AutoResizingTextarea value={item.answer ?? ""} readOnly />
</TabsContent>
</Tabs>
</div>
);
}
if (isWorkflowRunBlock(item)) {
if (
item.block_type === WorkflowBlockTypes.Task ||
item.block_type === WorkflowBlockTypes.Navigation ||
item.block_type === WorkflowBlockTypes.Action ||
item.block_type === WorkflowBlockTypes.Extraction ||
item.block_type === WorkflowBlockTypes.Validation ||
item.block_type === WorkflowBlockTypes.Login ||
item.block_type === WorkflowBlockTypes.FileDownload
) {
return (
<div className="rounded bg-slate-elevation1 p-4">
<Tabs key={item.block_type} defaultValue="navigation_goal">
<TabsList>
<TabsTrigger value="navigation_goal">Navigation Goal</TabsTrigger>
{item.status === Status.Completed && (
<TabsTrigger value="extracted_information">
Extracted Information
</TabsTrigger>
)}
{item.status && statusIsAFailureType({ status: item.status }) && (
<TabsTrigger value="failure_reason">Failure Reason</TabsTrigger>
)}
<TabsTrigger value="parameters">Parameters</TabsTrigger>
</TabsList>
<TabsContent value="navigation_goal">
<AutoResizingTextarea
value={item.navigation_goal ?? ""}
readOnly
/>
</TabsContent>
{item.status === Status.Completed && (
<TabsContent value="extracted_information">
<CodeEditor
language="json"
value={JSON.stringify(
(hasExtractedInformation(item.output) &&
item.output.extracted_information) ??
null,
null,
2,
)}
minHeight="96px"
maxHeight="500px"
readOnly
/>
</TabsContent>
)}
{item.status && statusIsAFailureType({ status: item.status }) && (
<TabsContent value="failure_reason">
<AutoResizingTextarea
value={
item.status === "canceled"
? "This block was cancelled"
: item.failure_reason ?? ""
}
readOnly
/>
</TabsContent>
)}
<TabsContent value="parameters">
<CodeEditor
value={JSON.stringify(item.navigation_payload, null, 2)}
minHeight="96px"
maxHeight="500px"
language="json"
readOnly
/>
</TabsContent>
</Tabs>
</div>
);
}
if (item.block_type === WorkflowBlockTypes.SendEmail) {
if (
item.body !== null &&
typeof item.body !== "undefined" &&
item.recipients !== null &&
typeof item.recipients !== "undefined"
) {
return (
<SendEmailBlockInfo body={item.body} recipients={item.recipients} />
);
}
return null;
}
if (item.block_type === WorkflowBlockTypes.TextPrompt) {
if (item.prompt !== null) {
return (
<div className="rounded bg-slate-elevation1 p-4">
<Tabs key={item.block_type} defaultValue="prompt">
<TabsList>
<TabsTrigger value="prompt">Prompt</TabsTrigger>
<TabsTrigger value="output">Output</TabsTrigger>
</TabsList>
<TabsContent value="prompt">
<CodeEditor
value={item.prompt ?? ""}
minHeight="96px"
maxHeight="500px"
readOnly
/>
</TabsContent>
<TabsContent value="output">
<CodeEditor
value={JSON.stringify(item.output, null, 2)}
minHeight="96px"
maxHeight="500px"
language="json"
readOnly
/>
</TabsContent>
</Tabs>
</div>
);
}
return null;
}
if (item.block_type === WorkflowBlockTypes.Wait) {
if (item.wait_sec !== null && typeof item.wait_sec !== "undefined") {
return (
<div className="flex w-1/2 justify-between rounded bg-slate-elevation1 p-4">
<span className="text-sm text-slate-400">Wait Time</span>
<span className="text-sm">{item.wait_sec} Seconds</span>
</div>
);
}
return null;
}
return (
<div className="rounded bg-slate-elevation1 p-4">
<Tabs key={item.block_type} defaultValue="output">
<TabsList>
<TabsTrigger value="output">Output</TabsTrigger>
</TabsList>
<TabsContent value="output">
<CodeEditor
value={JSON.stringify(item.output, null, 2)}
minHeight="96px"
maxHeight="500px"
language="json"
readOnly
/>
</TabsContent>
</Tabs>
</div>
);
}
}
export { WorkflowRunTimelineItemInfoSection };

View File

@@ -0,0 +1,29 @@
type Props = {
recipients: Array<string>;
body: string;
};
function SendEmailBlockInfo({ recipients, body }: Props) {
return (
<div className="flex gap-2">
<div className="w-1/2 space-y-4 p-4">
<div className="flex justify-between">
<span className="text-sm text-slate-400">From</span>
<span className="text-sm">hello@skyvern.com</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-slate-400">To</span>
{recipients.map((recipient) => {
return <span className="text-sm">{recipient}</span>;
})}
</div>
</div>
<div className="w-1/2 space-y-4 p-4">
<span className="text-sm text-slate-400">Body</span>
<p className="text-sm">{body}</p>
</div>
</div>
);
}
export { SendEmailBlockInfo };

View File

@@ -19,13 +19,4 @@ if (!artifactApiBaseUrl) {
console.warn("artifactApiBaseUrl environment variable was not set");
}
const observerEnabled = import.meta.env.VITE_OBSERVER_ENABLED as string;
const observerFeatureEnabled = observerEnabled === "true";
export {
apiBaseUrl,
environment,
envCredential,
artifactApiBaseUrl,
observerFeatureEnabled,
};
export { apiBaseUrl, environment, envCredential, artifactApiBaseUrl };