Workflow Run Timeline UI (#1433)
This commit is contained in:
@@ -223,6 +223,7 @@ export type TaskGenerationApiResponse = {
|
||||
};
|
||||
|
||||
export type ActionsApiResponse = {
|
||||
action_id: string;
|
||||
action_type: ActionType;
|
||||
status: Status;
|
||||
task_id: string | null;
|
||||
|
||||
39
skyvern-frontend/src/components/SwitchBarNavigation.tsx
Normal file
39
skyvern-frontend/src/components/SwitchBarNavigation.tsx
Normal 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 };
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
138
skyvern-frontend/src/routes/workflows/types/workflowRunTypes.ts
Normal file
138
skyvern-frontend/src/routes/workflows/types/workflowRunTypes.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user