Observer Timeline UI Updates (#1480)
This commit is contained in:
@@ -27,12 +27,17 @@ import {
|
||||
} from "@radix-ui/react-icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import fetchToCurl from "fetch-to-curl";
|
||||
import { Link, Outlet, useParams } from "react-router-dom";
|
||||
import { Link, Outlet, useParams, useSearchParams } from "react-router-dom";
|
||||
import { statusIsFinalized, statusIsRunningOrQueued } from "../tasks/types";
|
||||
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
|
||||
import { useWorkflowRunQuery } from "./hooks/useWorkflowRunQuery";
|
||||
import { WorkflowRunTimeline } from "./workflowRun/WorkflowRunTimeline";
|
||||
import { useWorkflowRunTimelineQuery } from "./hooks/useWorkflowRunTimelineQuery";
|
||||
import { findActiveItem } from "./workflowRun/workflowTimelineUtils";
|
||||
|
||||
function WorkflowRun() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const active = searchParams.get("active");
|
||||
const { workflowRunId, workflowPermanentId } = useParams();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const apiCredential = useApiCredential();
|
||||
@@ -45,6 +50,8 @@ function WorkflowRun() {
|
||||
const { data: workflowRun, isLoading: workflowRunIsLoading } =
|
||||
useWorkflowRunQuery();
|
||||
|
||||
const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery();
|
||||
|
||||
const cancelWorkflowMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
@@ -78,7 +85,11 @@ function WorkflowRun() {
|
||||
workflowRun && statusIsRunningOrQueued(workflowRun);
|
||||
|
||||
const workflowRunIsFinalized = workflowRun && statusIsFinalized(workflowRun);
|
||||
|
||||
const selection = findActiveItem(
|
||||
workflowRunTimeline ?? [],
|
||||
active,
|
||||
!!workflowRunIsFinalized,
|
||||
);
|
||||
const parameters = workflowRun?.parameters ?? {};
|
||||
const proxyLocation =
|
||||
workflowRun?.proxy_location ?? ProxyLocation.Residential;
|
||||
@@ -108,6 +119,13 @@ function WorkflowRun() {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
function handleSetActiveItem(id: string) {
|
||||
searchParams.set("active", id);
|
||||
setSearchParams(searchParams, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header className="flex justify-between">
|
||||
@@ -230,7 +248,28 @@ function WorkflowRun() {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Outlet />
|
||||
<div className="flex h-[42rem] gap-6">
|
||||
<div className="w-2/3">
|
||||
<Outlet />
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<WorkflowRunTimeline
|
||||
activeItem={selection}
|
||||
onActionItemSelected={(item) => {
|
||||
handleSetActiveItem(item.action.action_id);
|
||||
}}
|
||||
onBlockItemSelected={(item) => {
|
||||
handleSetActiveItem(item.workflow_run_block_id);
|
||||
}}
|
||||
onLiveStreamSelected={() => {
|
||||
handleSetActiveItem("stream");
|
||||
}}
|
||||
onObserverThoughtCardSelected={(item) => {
|
||||
handleSetActiveItem(item.observer_thought_id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,13 @@ export type WorkflowRunBlock = {
|
||||
wait_sec?: number | null;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
|
||||
// for loop block itself
|
||||
loop_values: Array<unknown> | null;
|
||||
|
||||
// for blocks in loop
|
||||
current_value: string | null;
|
||||
current_index: number | null;
|
||||
};
|
||||
|
||||
export type WorkflowRunTimelineBlockItem = {
|
||||
|
||||
@@ -3,21 +3,33 @@ 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";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type Props = {
|
||||
action: ActionsApiResponse;
|
||||
index: number;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
onClick: React.DOMAttributes<HTMLDivElement>["onClick"];
|
||||
};
|
||||
|
||||
function ActionCard({ action, onClick, active, index }: Props) {
|
||||
const success = action.status === Status.Completed;
|
||||
|
||||
const refCallback = useCallback((element: HTMLDivElement | null) => {
|
||||
if (element && active) {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
// this should only run once at mount.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex cursor-pointer rounded-lg border-2 bg-slate-elevation3 hover:border-slate-50",
|
||||
"flex cursor-pointer rounded-lg border-2 border-transparent bg-slate-elevation3 hover:border-slate-50",
|
||||
{
|
||||
"border-l-destructive": !success,
|
||||
"border-l-success": success,
|
||||
@@ -25,6 +37,7 @@ function ActionCard({ action, onClick, active, index }: Props) {
|
||||
},
|
||||
)}
|
||||
onClick={onClick}
|
||||
ref={refCallback}
|
||||
>
|
||||
<div className="flex-1 space-y-2 p-4 pl-5">
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { WorkflowRunBlock } from "../types/workflowRunTypes";
|
||||
import { isTaskVariantBlock, WorkflowBlockTypes } from "../types/workflowTypes";
|
||||
|
||||
type Props = {
|
||||
block: WorkflowRunBlock;
|
||||
};
|
||||
|
||||
function TaskBlockParameters({ block }: Props) {
|
||||
const isTaskVariant = isTaskVariantBlock(block);
|
||||
if (!isTaskVariant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showNavigationParameters =
|
||||
block.block_type === WorkflowBlockTypes.Task ||
|
||||
block.block_type === WorkflowBlockTypes.Action ||
|
||||
block.block_type === WorkflowBlockTypes.Login ||
|
||||
block.block_type === WorkflowBlockTypes.Navigation;
|
||||
|
||||
const showDataExtractionParameters =
|
||||
block.block_type === WorkflowBlockTypes.Task ||
|
||||
block.block_type === WorkflowBlockTypes.Extraction;
|
||||
|
||||
const showValidationParameters =
|
||||
block.block_type === WorkflowBlockTypes.Validation;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">URL</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
The starting URL for the block
|
||||
</h2>
|
||||
</div>
|
||||
<Input value={block.url ?? ""} readOnly />
|
||||
</div>
|
||||
|
||||
{showNavigationParameters ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Navigation Goal</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Where should Skyvern go and what should Skyvern do?
|
||||
</h2>
|
||||
</div>
|
||||
<AutoResizingTextarea value={block.navigation_goal ?? ""} readOnly />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showNavigationParameters ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Navigation Payload</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Specify important parameters, routes, or states
|
||||
</h2>
|
||||
</div>
|
||||
<CodeEditor
|
||||
className="w-full"
|
||||
language="json"
|
||||
value={JSON.stringify(block.navigation_payload, null, 2)}
|
||||
readOnly
|
||||
minHeight="96px"
|
||||
maxHeight="200px"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showDataExtractionParameters ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Data Extraction Goal</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
What outputs are you looking to get?
|
||||
</h2>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
value={block.data_extraction_goal ?? ""}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showDataExtractionParameters ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Data Schema</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Specify the output format in JSON
|
||||
</h2>
|
||||
</div>
|
||||
<CodeEditor
|
||||
className="w-full"
|
||||
language="json"
|
||||
value={JSON.stringify(block.data_schema, null, 2)}
|
||||
readOnly
|
||||
minHeight="96px"
|
||||
maxHeight="200px"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showValidationParameters ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Completion Criteria</h1>
|
||||
<h2 className="text-base text-slate-400">Complete if...</h2>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
value={block.complete_criterion ?? ""}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showValidationParameters ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Termination Criteria</h1>
|
||||
<h2 className="text-base text-slate-400">Terminate if...</h2>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
value={block.terminate_criterion ?? ""}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { TaskBlockParameters };
|
||||
@@ -1,6 +1,8 @@
|
||||
import { PersonIcon } from "@radix-ui/react-icons";
|
||||
import { QuestionMarkIcon } from "@radix-ui/react-icons";
|
||||
import { ObserverThought } from "../types/workflowRunTypes";
|
||||
import { cn } from "@/util/utils";
|
||||
import { BrainIcon } from "@/components/icons/BrainIcon";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type Props = {
|
||||
active: boolean;
|
||||
@@ -9,6 +11,17 @@ type Props = {
|
||||
};
|
||||
|
||||
function ThoughtCard({ thought, onClick, active }: Props) {
|
||||
const refCallback = useCallback((element: HTMLDivElement | null) => {
|
||||
if (element && active) {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
// this should only run once at mount.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -20,11 +33,15 @@ function ThoughtCard({ thought, onClick, active }: Props) {
|
||||
onClick={() => {
|
||||
onClick(thought);
|
||||
}}
|
||||
ref={refCallback}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<span>Thought</span>
|
||||
<div className="flex items-center gap-1 bg-slate-elevation5">
|
||||
<PersonIcon className="size-4" />
|
||||
<div className="flex gap-3">
|
||||
<BrainIcon className="size-6" />
|
||||
<span>Thinking</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-sm bg-slate-elevation5 px-2 py-1">
|
||||
<QuestionMarkIcon className="size-4" />
|
||||
<span className="text-xs">Decision</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,47 +1,165 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
|
||||
import { CodeEditor } from "../components/CodeEditor";
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
import { useActiveWorkflowRunItem } from "./useActiveWorkflowRunItem";
|
||||
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
|
||||
import { isAction, isWorkflowRunBlock } from "../types/workflowRunTypes";
|
||||
import { findBlockSurroundingAction } from "./workflowTimelineUtils";
|
||||
import { TaskBlockParameters } from "./TaskBlockParameters";
|
||||
import { isTaskVariantBlock, WorkflowBlockTypes } from "../types/workflowTypes";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ProxySelector } from "@/components/ProxySelector";
|
||||
import { SendEmailBlockParameters } from "./blockInfo/SendEmailBlockInfo";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
|
||||
function WorkflowPostRunParameters() {
|
||||
const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } =
|
||||
useWorkflowRunTimelineQuery();
|
||||
const [activeItem] = useActiveWorkflowRunItem();
|
||||
const { data: workflowRun, isLoading: workflowRunIsLoading } =
|
||||
useWorkflowRunQuery();
|
||||
const parameters = workflowRun?.parameters ?? {};
|
||||
|
||||
if (workflowRunIsLoading) {
|
||||
if (workflowRunIsLoading || workflowRunTimelineIsLoading) {
|
||||
return <div>Loading workflow parameters...</div>;
|
||||
}
|
||||
|
||||
return Object.entries(parameters).length > 0 ? (
|
||||
<div className="space-y-4 rounded-lg bg-slate-elevation3 px-6 py-5">
|
||||
<header>
|
||||
<h2 className="text-lg font-semibold">Input Parameter Values</h2>
|
||||
</header>
|
||||
{Object.entries(parameters).map(([key, value]) => {
|
||||
return (
|
||||
<div key={key} className="space-y-2">
|
||||
<Label className="text-lg">{key}</Label>
|
||||
{typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean" ? (
|
||||
<AutoResizingTextarea value={String(value)} readOnly />
|
||||
) : (
|
||||
<CodeEditor
|
||||
value={JSON.stringify(value, null, 2)}
|
||||
readOnly
|
||||
language="json"
|
||||
minHeight="96px"
|
||||
maxHeight="500px"
|
||||
/>
|
||||
)}
|
||||
if (!workflowRun || !workflowRunTimeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getActiveBlock() {
|
||||
if (!workflowRunTimeline) {
|
||||
return;
|
||||
}
|
||||
if (isWorkflowRunBlock(activeItem)) {
|
||||
return activeItem;
|
||||
}
|
||||
if (isAction(activeItem)) {
|
||||
return findBlockSurroundingAction(
|
||||
workflowRunTimeline,
|
||||
activeItem.action_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const activeBlock = getActiveBlock();
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{activeBlock && isTaskVariantBlock(activeBlock) ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Block Parameters</h1>
|
||||
<TaskBlockParameters block={activeBlock} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{activeBlock &&
|
||||
activeBlock.block_type === WorkflowBlockTypes.SendEmail ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Block Parameters</h1>
|
||||
<SendEmailBlockParameters
|
||||
body={activeBlock.body ?? ""}
|
||||
recipients={activeBlock.recipients ?? []}
|
||||
subject={activeBlock.subject ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{activeBlock && activeBlock.block_type === WorkflowBlockTypes.ForLoop ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Block Parameters</h1>
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Loop Values</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
The values that are being looped over
|
||||
</h2>
|
||||
</div>
|
||||
<CodeEditor
|
||||
className="w-full"
|
||||
language="json"
|
||||
value={JSON.stringify(activeBlock.loop_values, null, 2)}
|
||||
readOnly
|
||||
minHeight="96px"
|
||||
maxHeight="200px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Workflow Input Parameters</h1>
|
||||
{Object.entries(parameters).map(([key, value]) => {
|
||||
return (
|
||||
<div key={key} className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">{key}</h1>
|
||||
</div>
|
||||
{typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean" ? (
|
||||
<AutoResizingTextarea value={String(value)} readOnly />
|
||||
) : (
|
||||
<CodeEditor
|
||||
value={JSON.stringify(value, null, 2)}
|
||||
readOnly
|
||||
language="json"
|
||||
minHeight="96px"
|
||||
maxHeight="200px"
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{Object.entries(parameters).length === 0 ? (
|
||||
<div>No input parameters found for this workflow</div>
|
||||
) : null}
|
||||
<h1 className="text-lg font-bold">Other Workflow Parameters</h1>
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Webhook Callback URL</h1>
|
||||
</div>
|
||||
<Input value={workflowRun.webhook_callback_url ?? ""} readOnly />
|
||||
</div>
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Proxy Location</h1>
|
||||
</div>
|
||||
<ProxySelector
|
||||
value={workflowRun.proxy_location ?? ProxyLocation.Residential}
|
||||
onChange={() => {
|
||||
// TODO
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{workflowRun.observer_cruise ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Observer Parameters</h1>
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Observer Prompt</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
The original prompt for the observer
|
||||
</h2>
|
||||
</div>
|
||||
<Input
|
||||
value={workflowRun.observer_cruise.prompt ?? ""}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(parameters).length === 0 && (
|
||||
<div>This workflow doesn't have any input parameters.</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,128 @@
|
||||
import { FileIcon } from "@radix-ui/react-icons";
|
||||
import { CodeEditor } from "../components/CodeEditor";
|
||||
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
|
||||
import { useActiveWorkflowRunItem } from "./useActiveWorkflowRunItem";
|
||||
import {
|
||||
hasExtractedInformation,
|
||||
isAction,
|
||||
isWorkflowRunBlock,
|
||||
} from "../types/workflowRunTypes";
|
||||
import { findBlockSurroundingAction } from "./workflowTimelineUtils";
|
||||
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
|
||||
import { Status } from "@/api/types";
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
|
||||
function WorkflowRunOutput() {
|
||||
const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } =
|
||||
useWorkflowRunTimelineQuery();
|
||||
const [activeItem] = useActiveWorkflowRunItem();
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
|
||||
if (workflowRunTimelineIsLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!workflowRunTimeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getActiveBlock() {
|
||||
if (!workflowRunTimeline) {
|
||||
return;
|
||||
}
|
||||
if (isWorkflowRunBlock(activeItem)) {
|
||||
return activeItem;
|
||||
}
|
||||
if (isAction(activeItem)) {
|
||||
return findBlockSurroundingAction(
|
||||
workflowRunTimeline,
|
||||
activeItem.action_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const activeBlock = getActiveBlock();
|
||||
|
||||
const showExtractedInformation =
|
||||
activeBlock && activeBlock.status === Status.Completed;
|
||||
|
||||
const outputs = workflowRun?.outputs;
|
||||
const fileUrls = workflowRun?.downloaded_file_urls ?? [];
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<header>
|
||||
<h2 className="text-lg font-semibold">Workflow Run Output</h2>
|
||||
</header>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={
|
||||
outputs ? JSON.stringify(outputs, null, 2) : "Waiting for outputs.."
|
||||
}
|
||||
readOnly
|
||||
minHeight="96px"
|
||||
maxHeight="500px"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<header>
|
||||
<h2 className="text-lg font-semibold">Downloaded Files</h2>
|
||||
</header>
|
||||
<div className="space-y-2">
|
||||
{fileUrls.length > 0 ? (
|
||||
fileUrls.map((url, index) => {
|
||||
return (
|
||||
<div key={url} title={url} className="flex gap-2">
|
||||
<FileIcon className="size-6" />
|
||||
<a href={url} className="underline underline-offset-4">
|
||||
<span>{`File ${index + 1}`}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div>No files downloaded</div>
|
||||
)}
|
||||
<div className="space-y-5">
|
||||
{activeBlock ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Block Outputs</h1>
|
||||
<div className="space-y-2">
|
||||
<h2>
|
||||
{showExtractedInformation
|
||||
? "Extracted Information"
|
||||
: "Failure Reason"}
|
||||
</h2>
|
||||
{showExtractedInformation ? (
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={JSON.stringify(
|
||||
(hasExtractedInformation(activeBlock.output) &&
|
||||
activeBlock.output.extracted_information) ??
|
||||
null,
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
minHeight="96px"
|
||||
maxHeight="200px"
|
||||
readOnly
|
||||
/>
|
||||
) : (
|
||||
<AutoResizingTextarea
|
||||
value={
|
||||
activeBlock.status === "canceled"
|
||||
? "This block was cancelled"
|
||||
: activeBlock.failure_reason ?? ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Workflow Run Outputs</h1>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={
|
||||
outputs
|
||||
? JSON.stringify(outputs, null, 2)
|
||||
: "Waiting for outputs.."
|
||||
}
|
||||
readOnly
|
||||
minHeight="96px"
|
||||
maxHeight="200px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Workflow Run Downloaded Files</h1>
|
||||
<div className="space-y-2">
|
||||
{fileUrls.length > 0 ? (
|
||||
fileUrls.map((url, index) => {
|
||||
return (
|
||||
<div key={url} title={url} className="flex gap-2">
|
||||
<FileIcon className="size-6" />
|
||||
<a href={url} className="underline underline-offset-4">
|
||||
<span>{`File ${index + 1}`}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div>No files downloaded</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
import { ActionsApiResponse } from "@/api/types";
|
||||
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 { statusIsFinalized } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
|
||||
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
|
||||
import {
|
||||
isActionItem,
|
||||
isBlockItem,
|
||||
isAction,
|
||||
isObserverThought,
|
||||
isTaskVariantBlockItem,
|
||||
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";
|
||||
import { WorkflowRunBlockScreenshot } from "./WorkflowRunBlockScreenshot";
|
||||
|
||||
const formatter = Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
});
|
||||
import { WorkflowRunStream } from "./WorkflowRunStream";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { findActiveItem } from "./workflowTimelineUtils";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export type ActionItem = {
|
||||
block: WorkflowRunBlock;
|
||||
@@ -37,14 +24,15 @@ export type ActionItem = {
|
||||
};
|
||||
|
||||
export type WorkflowRunOverviewActiveElement =
|
||||
| ActionItem
|
||||
| ActionsApiResponse
|
||||
| ObserverThought
|
||||
| WorkflowRunBlock
|
||||
| "stream"
|
||||
| null;
|
||||
|
||||
function WorkflowRunOverview() {
|
||||
const [active, setActive] = useState<WorkflowRunOverviewActiveElement>(null);
|
||||
const [searchParams] = useSearchParams();
|
||||
const active = searchParams.get("active");
|
||||
const { data: workflowRun, isLoading: workflowRunIsLoading } =
|
||||
useWorkflowRunQuery();
|
||||
|
||||
@@ -52,7 +40,11 @@ function WorkflowRunOverview() {
|
||||
useWorkflowRunTimelineQuery();
|
||||
|
||||
if (workflowRunIsLoading || workflowRunTimelineIsLoading) {
|
||||
return <WorkflowRunOverviewSkeleton />;
|
||||
return (
|
||||
<AspectRatio ratio={16 / 9}>
|
||||
<Skeleton className="h-full w-full" />
|
||||
</AspectRatio>
|
||||
);
|
||||
}
|
||||
|
||||
if (!workflowRun) {
|
||||
@@ -63,141 +55,33 @@ function WorkflowRunOverview() {
|
||||
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
|
||||
) {
|
||||
const last = timelineItem.block.actions.length - 1;
|
||||
const actionItem: ActionItem = {
|
||||
block: timelineItem.block,
|
||||
action: timelineItem.block.actions[last]!,
|
||||
};
|
||||
return actionItem;
|
||||
}
|
||||
return timelineItem.block;
|
||||
}
|
||||
if (isThoughtItem(timelineItem)) {
|
||||
return timelineItem.thought;
|
||||
}
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
const selection = getActiveSelection();
|
||||
|
||||
const numberOfActions = workflowRunTimeline.reduce((total, current) => {
|
||||
if (isTaskVariantBlockItem(current)) {
|
||||
return total + current.block!.actions!.length;
|
||||
}
|
||||
return total + 0;
|
||||
}, 0);
|
||||
const workflowRunIsFinalized = statusIsFinalized(workflowRun);
|
||||
const selection = findActiveItem(
|
||||
workflowRunTimeline,
|
||||
active,
|
||||
workflowRunIsFinalized,
|
||||
);
|
||||
|
||||
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" && isActionItem(selection) && (
|
||||
<ActionScreenshot
|
||||
index={selection.action.action_order ?? 0}
|
||||
stepId={selection.action.step_id ?? ""}
|
||||
/>
|
||||
)}
|
||||
{isWorkflowRunBlock(selection) && (
|
||||
<WorkflowRunBlockScreenshot
|
||||
workflowRunBlockId={selection.workflow_run_block_id}
|
||||
/>
|
||||
)}
|
||||
{isObserverThought(selection) && (
|
||||
<ObserverThoughtScreenshot
|
||||
observerThoughtId={selection.observer_thought_id}
|
||||
/>
|
||||
)}
|
||||
</AspectRatio>
|
||||
|
||||
<WorkflowRunTimelineItemInfoSection activeItem={selection} />
|
||||
</div>
|
||||
<div className="w-1/3 min-w-0 space-y-4 rounded bg-slate-elevation1 p-4">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||
Actions: {numberOfActions}
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||
Steps: {workflowRun.total_steps ?? 0}
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||
Cost: {formatter.format(workflowRun.total_cost ?? 0)}
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea>
|
||||
<ScrollAreaViewport className="max-h-[37rem]">
|
||||
<div className="space-y-4">
|
||||
{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>
|
||||
<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) && (
|
||||
<WorkflowRunBlockScreenshot
|
||||
workflowRunBlockId={selection.workflow_run_block_id}
|
||||
/>
|
||||
)}
|
||||
{isObserverThought(selection) && (
|
||||
<ObserverThoughtScreenshot
|
||||
observerThoughtId={selection.observer_thought_id}
|
||||
/>
|
||||
)}
|
||||
</AspectRatio>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
|
||||
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
|
||||
import {
|
||||
isBlockItem,
|
||||
isObserverThought,
|
||||
isTaskVariantBlockItem,
|
||||
isThoughtItem,
|
||||
ObserverThought,
|
||||
WorkflowRunBlock,
|
||||
} from "../types/workflowRunTypes";
|
||||
import {
|
||||
ActionItem,
|
||||
WorkflowRunOverviewActiveElement,
|
||||
} from "./WorkflowRunOverview";
|
||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||
import { statusIsNotFinalized } from "@/routes/tasks/types";
|
||||
import { cn } from "@/util/utils";
|
||||
import { ThoughtCard } from "./ThoughtCard";
|
||||
import { WorkflowRunTimelineBlockItem } from "./WorkflowRunTimelineBlockItem";
|
||||
import { DotFilledIcon } from "@radix-ui/react-icons";
|
||||
|
||||
const formatter = Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
});
|
||||
|
||||
type Props = {
|
||||
activeItem: WorkflowRunOverviewActiveElement;
|
||||
onLiveStreamSelected: () => void;
|
||||
onObserverThoughtCardSelected: (item: ObserverThought) => void;
|
||||
onActionItemSelected: (item: ActionItem) => void;
|
||||
onBlockItemSelected: (item: WorkflowRunBlock) => void;
|
||||
};
|
||||
|
||||
function WorkflowRunTimeline({
|
||||
activeItem,
|
||||
onLiveStreamSelected,
|
||||
onObserverThoughtCardSelected,
|
||||
onActionItemSelected,
|
||||
onBlockItemSelected,
|
||||
}: Props) {
|
||||
const { data: workflowRun, isLoading: workflowRunIsLoading } =
|
||||
useWorkflowRunQuery();
|
||||
|
||||
const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } =
|
||||
useWorkflowRunTimelineQuery();
|
||||
|
||||
if (workflowRunIsLoading || workflowRunTimelineIsLoading) {
|
||||
return <Skeleton className="h-full w-full" />;
|
||||
}
|
||||
|
||||
if (!workflowRun || !workflowRunTimeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workflowRunIsNotFinalized = statusIsNotFinalized(workflowRun);
|
||||
|
||||
const timeline = workflowRunTimeline.slice().reverse();
|
||||
|
||||
const numberOfActions = workflowRunTimeline.reduce((total, current) => {
|
||||
if (isTaskVariantBlockItem(current)) {
|
||||
return total + current.block!.actions!.length;
|
||||
}
|
||||
return total + 0;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="min-w-0 space-y-4 rounded bg-slate-elevation1 p-4">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||
Actions: {numberOfActions}
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||
Steps: {workflowRun.total_steps ?? 0}
|
||||
</div>
|
||||
<div className="flex items-center justify-center rounded bg-slate-elevation3 px-4 py-3 text-xs">
|
||||
Cost: {formatter.format(workflowRun.total_cost ?? 0)}
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea>
|
||||
<ScrollAreaViewport className="max-h-[37rem]">
|
||||
<div className="space-y-4">
|
||||
{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": activeItem === "stream",
|
||||
},
|
||||
)}
|
||||
onClick={onLiveStreamSelected}
|
||||
>
|
||||
<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={activeItem}
|
||||
block={timelineItem.block}
|
||||
onActionClick={onActionItemSelected}
|
||||
onBlockItemClick={onBlockItemSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isThoughtItem(timelineItem)) {
|
||||
return (
|
||||
<ThoughtCard
|
||||
key={timelineItem.thought.observer_thought_id}
|
||||
active={
|
||||
isObserverThought(activeItem) &&
|
||||
activeItem.observer_thought_id ===
|
||||
timelineItem.thought.observer_thought_id
|
||||
}
|
||||
onClick={onObserverThoughtCardSelected}
|
||||
thought={timelineItem.thought}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</ScrollAreaViewport>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowRunTimeline };
|
||||
@@ -1,14 +1,20 @@
|
||||
import { CubeIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
||||
import { workflowBlockTitle } from "../editor/nodes/types";
|
||||
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
|
||||
import {
|
||||
isActionItem,
|
||||
isAction,
|
||||
isWorkflowRunBlock,
|
||||
WorkflowRunBlock,
|
||||
} from "../types/workflowRunTypes";
|
||||
import { ActionCard } from "./ActionCard";
|
||||
import { BlockCard } from "./BlockCard";
|
||||
import {
|
||||
ActionItem,
|
||||
WorkflowRunOverviewActiveElement,
|
||||
} from "./WorkflowRunOverview";
|
||||
import { cn } from "@/util/utils";
|
||||
import { isTaskVariantBlock } from "../types/workflowTypes";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type Props = {
|
||||
activeItem: WorkflowRunOverviewActiveElement;
|
||||
@@ -27,19 +33,86 @@ function WorkflowRunTimelineBlockItem({
|
||||
}: Props) {
|
||||
const actions = block.actions ? [...block.actions].reverse() : [];
|
||||
|
||||
const hasActiveAction =
|
||||
isAction(activeItem) &&
|
||||
Boolean(
|
||||
block.actions?.find(
|
||||
(action) => action.action_id === activeItem.action_id,
|
||||
),
|
||||
);
|
||||
const isActiveBlock =
|
||||
isWorkflowRunBlock(activeItem) &&
|
||||
activeItem.workflow_run_block_id === block.workflow_run_block_id;
|
||||
|
||||
const showDiagnosticLink =
|
||||
isTaskVariantBlock(block) && (hasActiveAction || isActiveBlock);
|
||||
|
||||
const refCallback = useCallback((element: HTMLDivElement | null) => {
|
||||
if (
|
||||
element &&
|
||||
isWorkflowRunBlock(activeItem) &&
|
||||
activeItem.workflow_run_block_id === block.workflow_run_block_id
|
||||
) {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
// this should only run once at mount.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 rounded border border-slate-600 p-4">
|
||||
<div
|
||||
className={cn(
|
||||
"cursor-pointer space-y-4 rounded border border-slate-600 p-4",
|
||||
{
|
||||
"border-slate-50":
|
||||
isWorkflowRunBlock(activeItem) &&
|
||||
activeItem.workflow_run_block_id === block.workflow_run_block_id,
|
||||
},
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onBlockItemClick(block);
|
||||
}}
|
||||
ref={refCallback}
|
||||
>
|
||||
<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">
|
||||
{showDiagnosticLink ? (
|
||||
<Link to={`/tasks/${block.task_id}/diagnostics`}>
|
||||
<div className="flex gap-1">
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
<span className="text-xs">Diagnostics</span>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<CubeIcon className="size-4" />
|
||||
<span className="text-xs">Block</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{actions.map((action, index) => {
|
||||
return (
|
||||
<ActionCard
|
||||
key={action.action_id}
|
||||
action={action}
|
||||
active={
|
||||
isActionItem(activeItem) &&
|
||||
activeItem.action.action_id === action.action_id
|
||||
isAction(activeItem) && activeItem.action_id === action.action_id
|
||||
}
|
||||
index={actions.length - index}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const actionItem: ActionItem = {
|
||||
block,
|
||||
action,
|
||||
@@ -61,16 +134,6 @@ function WorkflowRunTimelineBlockItem({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<BlockCard
|
||||
active={
|
||||
isWorkflowRunBlock(activeItem) &&
|
||||
activeItem.workflow_run_block_id === block.workflow_run_block_id
|
||||
}
|
||||
block={block}
|
||||
onClick={() => {
|
||||
onBlockItemClick(block);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ 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";
|
||||
import { WorkflowRunOverviewActiveElement } from "./WorkflowRunOverview";
|
||||
import { ExternalLinkIcon } from "@radix-ui/react-icons";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SendEmailBlockParameters } from "./blockInfo/SendEmailBlockInfo";
|
||||
|
||||
type Props = {
|
||||
activeItem: WorkflowRunOverviewActiveElement;
|
||||
@@ -151,10 +151,16 @@ function WorkflowRunTimelineItemInfoSection({ activeItem }: Props) {
|
||||
item.body !== null &&
|
||||
typeof item.body !== "undefined" &&
|
||||
item.recipients !== null &&
|
||||
typeof item.recipients !== "undefined"
|
||||
typeof item.recipients !== "undefined" &&
|
||||
item.subject !== null &&
|
||||
typeof item.subject !== "undefined"
|
||||
) {
|
||||
return (
|
||||
<SendEmailBlockInfo body={item.body} recipients={item.recipients} />
|
||||
<SendEmailBlockParameters
|
||||
body={item.body}
|
||||
recipients={item.recipients}
|
||||
subject={item.subject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
type Props = {
|
||||
recipients: Array<string>;
|
||||
body: string;
|
||||
subject: string;
|
||||
};
|
||||
|
||||
function SendEmailBlockInfo({ recipients, body }: Props) {
|
||||
function SendEmailBlockParameters({ recipients, body, subject }: 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 className="space-y-4">
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">To</h1>
|
||||
</div>
|
||||
<Input value={recipients.join(", ")} readOnly />
|
||||
</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 className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Subject</h1>
|
||||
</div>
|
||||
<Input value={subject} readOnly />
|
||||
</div>
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Body</h1>
|
||||
</div>
|
||||
<AutoResizingTextarea value={body} readOnly />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SendEmailBlockInfo };
|
||||
export { SendEmailBlockParameters };
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
|
||||
import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery";
|
||||
import { statusIsFinalized } from "@/routes/tasks/types";
|
||||
import { findActiveItem } from "./workflowTimelineUtils";
|
||||
import { WorkflowRunOverviewActiveElement } from "./WorkflowRunOverview";
|
||||
|
||||
function useActiveWorkflowRunItem(): [
|
||||
WorkflowRunOverviewActiveElement,
|
||||
(item: string) => void,
|
||||
] {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const active = searchParams.get("active");
|
||||
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
|
||||
const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery();
|
||||
|
||||
const workflowRunIsFinalized = workflowRun && statusIsFinalized(workflowRun);
|
||||
const activeItem = findActiveItem(
|
||||
workflowRunTimeline ?? [],
|
||||
active,
|
||||
!!workflowRunIsFinalized,
|
||||
);
|
||||
|
||||
function handleSetActiveItem(id: string) {
|
||||
searchParams.set("active", id);
|
||||
setSearchParams(searchParams, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
|
||||
return [activeItem, handleSetActiveItem];
|
||||
}
|
||||
|
||||
export { useActiveWorkflowRunItem };
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
isBlockItem,
|
||||
isThoughtItem,
|
||||
WorkflowRunBlock,
|
||||
WorkflowRunTimelineItem,
|
||||
} from "../types/workflowRunTypes";
|
||||
import { WorkflowRunOverviewActiveElement } from "./WorkflowRunOverview";
|
||||
|
||||
function findBlockSurroundingAction(
|
||||
timeline: Array<WorkflowRunTimelineItem>,
|
||||
actionId: string,
|
||||
): WorkflowRunBlock | undefined {
|
||||
const stack = [...timeline];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
if (current.type === "block") {
|
||||
const action = current.block.actions?.find(
|
||||
(action) => action.action_id === actionId,
|
||||
);
|
||||
if (action) {
|
||||
return current.block;
|
||||
}
|
||||
}
|
||||
if (current.children) {
|
||||
stack.push(...current.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findActiveItem(
|
||||
timeline: Array<WorkflowRunTimelineItem>,
|
||||
target: string | null,
|
||||
workflowRunIsFinalized: boolean,
|
||||
): WorkflowRunOverviewActiveElement {
|
||||
if (target === null) {
|
||||
if (!workflowRunIsFinalized) {
|
||||
return "stream";
|
||||
}
|
||||
if (timeline?.length > 0) {
|
||||
const last = timeline.length - 1;
|
||||
const timelineItem = timeline![last];
|
||||
if (isBlockItem(timelineItem)) {
|
||||
if (
|
||||
timelineItem.block.actions &&
|
||||
timelineItem.block.actions.length > 0
|
||||
) {
|
||||
const last = timelineItem.block.actions.length - 1;
|
||||
return timelineItem.block.actions[last]!;
|
||||
}
|
||||
return timelineItem.block;
|
||||
}
|
||||
if (isThoughtItem(timelineItem)) {
|
||||
return timelineItem.thought;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target === "stream") {
|
||||
return "stream";
|
||||
}
|
||||
const stack = [...timeline];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()!;
|
||||
if (
|
||||
current.type === "block" &&
|
||||
current.block.workflow_run_block_id === target
|
||||
) {
|
||||
return current.block;
|
||||
}
|
||||
if (
|
||||
current.type === "thought" &&
|
||||
current.thought.observer_thought_id === target
|
||||
) {
|
||||
return current.thought;
|
||||
}
|
||||
if (current.type === "block") {
|
||||
const actions = current.block.actions;
|
||||
if (actions) {
|
||||
const activeAction = actions.find(
|
||||
(action) => action.action_id === target,
|
||||
);
|
||||
if (activeAction) {
|
||||
return activeAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current.children) {
|
||||
stack.push(...current.children);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export { findActiveItem, findBlockSurroundingAction };
|
||||
Reference in New Issue
Block a user