show metadata for non-navigation blocks on workflow parameters page (#4243)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { useWorkflowRunWithWorkflowQuery } from "../hooks/useWorkflowRunWithWorkflowQuery";
|
||||
import { CodeEditor } from "../components/CodeEditor";
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
@@ -6,12 +7,21 @@ import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuer
|
||||
import { isAction, isWorkflowRunBlock } from "../types/workflowRunTypes";
|
||||
import { findBlockSurroundingAction } from "./workflowTimelineUtils";
|
||||
import { TaskBlockParameters } from "./TaskBlockParameters";
|
||||
import { isTaskVariantBlock, WorkflowBlockTypes } from "../types/workflowTypes";
|
||||
import {
|
||||
isTaskVariantBlock,
|
||||
WorkflowBlockTypes,
|
||||
type WorkflowBlock,
|
||||
type WorkflowBlockType,
|
||||
} from "../types/workflowTypes";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ProxySelector } from "@/components/ProxySelector";
|
||||
import { SendEmailBlockParameters } from "./blockInfo/SendEmailBlockInfo";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
import { KeyValueInput } from "@/components/KeyValueInput";
|
||||
import { CodeBlockParameters } from "./blockInfo/CodeBlockParameters";
|
||||
import { TextPromptBlockParameters } from "./blockInfo/TextPromptBlockParameters";
|
||||
import { GotoUrlBlockParameters } from "./blockInfo/GotoUrlBlockParameters";
|
||||
import { FileDownloadBlockParameters } from "./blockInfo/FileDownloadBlockParameters";
|
||||
|
||||
function WorkflowPostRunParameters() {
|
||||
const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } =
|
||||
@@ -20,14 +30,7 @@ function WorkflowPostRunParameters() {
|
||||
const { data: workflowRun, isLoading: workflowRunIsLoading } =
|
||||
useWorkflowRunWithWorkflowQuery();
|
||||
const parameters = workflowRun?.parameters ?? {};
|
||||
|
||||
if (workflowRunIsLoading || workflowRunTimelineIsLoading) {
|
||||
return <div>Loading workflow parameters...</div>;
|
||||
}
|
||||
|
||||
if (!workflowRun || !workflowRunTimeline) {
|
||||
return null;
|
||||
}
|
||||
const workflow = workflowRun?.workflow;
|
||||
|
||||
function getActiveBlock() {
|
||||
if (!workflowRunTimeline) {
|
||||
@@ -45,19 +48,37 @@ function WorkflowPostRunParameters() {
|
||||
}
|
||||
|
||||
const activeBlock = getActiveBlock();
|
||||
const isTaskV2 = workflowRun.task_v2 !== null;
|
||||
const activeBlockLabel = activeBlock?.label ?? null;
|
||||
const definitionBlock = useMemo(() => {
|
||||
if (!workflow || !activeBlockLabel) {
|
||||
return null;
|
||||
}
|
||||
return findWorkflowBlockByLabel(
|
||||
workflow.workflow_definition.blocks,
|
||||
activeBlockLabel,
|
||||
);
|
||||
}, [workflow, activeBlockLabel]);
|
||||
const isTaskV2 = Boolean(workflowRun?.task_v2);
|
||||
|
||||
const webhookCallbackUrl = isTaskV2
|
||||
? workflowRun.task_v2?.webhook_callback_url
|
||||
: workflowRun.webhook_callback_url;
|
||||
? workflowRun?.task_v2?.webhook_callback_url ?? null
|
||||
: workflowRun?.webhook_callback_url ?? null;
|
||||
|
||||
const proxyLocation = isTaskV2
|
||||
? workflowRun.task_v2?.proxy_location
|
||||
: workflowRun.proxy_location;
|
||||
? workflowRun?.task_v2?.proxy_location ?? null
|
||||
: workflowRun?.proxy_location ?? null;
|
||||
|
||||
const extraHttpHeaders = isTaskV2
|
||||
? workflowRun.task_v2?.extra_http_headers
|
||||
: workflowRun.extra_http_headers;
|
||||
? workflowRun?.task_v2?.extra_http_headers ?? null
|
||||
: workflowRun?.extra_http_headers ?? null;
|
||||
|
||||
if (workflowRunIsLoading || workflowRunTimelineIsLoading) {
|
||||
return <div>Loading workflow parameters...</div>;
|
||||
}
|
||||
|
||||
if (!workflowRun || !workflowRunTimeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -70,6 +91,27 @@ function WorkflowPostRunParameters() {
|
||||
</div>
|
||||
) : null}
|
||||
{activeBlock &&
|
||||
activeBlock.block_type === WorkflowBlockTypes.FileDownload &&
|
||||
isBlockOfType(definitionBlock, WorkflowBlockTypes.FileDownload) ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">File Download Settings</h1>
|
||||
<FileDownloadBlockParameters
|
||||
prompt={
|
||||
activeBlock.navigation_goal ??
|
||||
definitionBlock.navigation_goal ??
|
||||
null
|
||||
}
|
||||
downloadSuffix={definitionBlock.download_suffix ?? null}
|
||||
downloadTimeout={definitionBlock.download_timeout ?? null}
|
||||
errorCodeMapping={definitionBlock.error_code_mapping ?? null}
|
||||
maxRetries={definitionBlock.max_retries ?? null}
|
||||
maxStepsPerRun={definitionBlock.max_steps_per_run ?? null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{activeBlock &&
|
||||
activeBlock.block_type === WorkflowBlockTypes.SendEmail ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
@@ -105,6 +147,50 @@ function WorkflowPostRunParameters() {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{activeBlock &&
|
||||
activeBlock.block_type === WorkflowBlockTypes.Code &&
|
||||
isBlockOfType(definitionBlock, WorkflowBlockTypes.Code) ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Code Block</h1>
|
||||
<CodeBlockParameters
|
||||
code={definitionBlock.code}
|
||||
parameters={definitionBlock.parameters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{activeBlock &&
|
||||
activeBlock.block_type === WorkflowBlockTypes.TextPrompt &&
|
||||
isBlockOfType(definitionBlock, WorkflowBlockTypes.TextPrompt) ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Text Prompt Block</h1>
|
||||
<TextPromptBlockParameters
|
||||
prompt={activeBlock.prompt ?? definitionBlock.prompt ?? ""}
|
||||
llmKey={definitionBlock.llm_key}
|
||||
jsonSchema={definitionBlock.json_schema}
|
||||
parameters={definitionBlock.parameters}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{activeBlock && activeBlock.block_type === WorkflowBlockTypes.URL ? (
|
||||
<div className="rounded bg-slate-elevation2 p-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-bold">Go To URL Block</h1>
|
||||
<GotoUrlBlockParameters
|
||||
url={
|
||||
activeBlock.url ??
|
||||
(isBlockOfType(definitionBlock, WorkflowBlockTypes.URL)
|
||||
? definitionBlock.url
|
||||
: "")
|
||||
}
|
||||
continueOnFailure={activeBlock.continue_on_failure}
|
||||
/>
|
||||
</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>
|
||||
@@ -192,3 +278,31 @@ function WorkflowPostRunParameters() {
|
||||
}
|
||||
|
||||
export { WorkflowPostRunParameters };
|
||||
|
||||
function findWorkflowBlockByLabel(
|
||||
blocks: Array<WorkflowBlock>,
|
||||
label: string,
|
||||
): WorkflowBlock | null {
|
||||
for (const block of blocks) {
|
||||
if (block.label === label) {
|
||||
return block;
|
||||
}
|
||||
if (
|
||||
block.block_type === WorkflowBlockTypes.ForLoop &&
|
||||
block.loop_blocks.length > 0
|
||||
) {
|
||||
const nested = findWorkflowBlockByLabel(block.loop_blocks, label);
|
||||
if (nested) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isBlockOfType<T extends WorkflowBlockType>(
|
||||
block: WorkflowBlock | null,
|
||||
type: T,
|
||||
): block is Extract<WorkflowBlock, { block_type: T }> {
|
||||
return block?.block_type === type;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import type { WorkflowParameter } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
type Props = {
|
||||
code: string;
|
||||
parameters?: Array<WorkflowParameter>;
|
||||
};
|
||||
|
||||
function CodeBlockParameters({ code, parameters }: Props) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Code</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
The Python snippet executed for this block
|
||||
</h2>
|
||||
</div>
|
||||
<CodeEditor
|
||||
className="w-full"
|
||||
language="python"
|
||||
value={code}
|
||||
readOnly
|
||||
minHeight="160px"
|
||||
maxHeight="400px"
|
||||
/>
|
||||
</div>
|
||||
{parameters && parameters.length > 0 ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Parameters</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Inputs passed to this code block
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
{parameters.map((parameter) => (
|
||||
<div
|
||||
key={parameter.key}
|
||||
className="rounded border border-slate-700/40 bg-slate-elevation3 p-3"
|
||||
>
|
||||
<p className="font-medium">{parameter.key}</p>
|
||||
{parameter.description ? (
|
||||
<p className="text-sm text-slate-400">
|
||||
{parameter.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CodeBlockParameters };
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
|
||||
type Props = {
|
||||
prompt?: string | null;
|
||||
downloadSuffix?: string | null;
|
||||
downloadTimeout?: number | null;
|
||||
errorCodeMapping?: Record<string, string> | null;
|
||||
maxRetries?: number | null;
|
||||
maxStepsPerRun?: number | null;
|
||||
};
|
||||
|
||||
function FileDownloadBlockParameters({
|
||||
prompt,
|
||||
downloadSuffix,
|
||||
downloadTimeout,
|
||||
errorCodeMapping,
|
||||
maxRetries,
|
||||
maxStepsPerRun,
|
||||
}: Props) {
|
||||
const formattedErrorCodeMapping = errorCodeMapping
|
||||
? JSON.stringify(errorCodeMapping, null, 2)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{prompt ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Prompt</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Instructions followed to download the file
|
||||
</h2>
|
||||
</div>
|
||||
<AutoResizingTextarea value={prompt} readOnly />
|
||||
</div>
|
||||
) : null}
|
||||
{downloadSuffix ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Download Suffix</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Expected suffix or filename for the downloaded file
|
||||
</h2>
|
||||
</div>
|
||||
<Input value={downloadSuffix} readOnly />
|
||||
</div>
|
||||
) : null}
|
||||
{typeof downloadTimeout === "number" ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Download Timeout</h1>
|
||||
<h2 className="text-base text-slate-400">In seconds</h2>
|
||||
</div>
|
||||
<Input value={downloadTimeout.toString()} readOnly />
|
||||
</div>
|
||||
) : null}
|
||||
{typeof maxRetries === "number" ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Max Retries</h1>
|
||||
</div>
|
||||
<Input value={maxRetries.toString()} readOnly />
|
||||
</div>
|
||||
) : null}
|
||||
{typeof maxStepsPerRun === "number" ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Max Steps Per Run</h1>
|
||||
</div>
|
||||
<Input value={maxStepsPerRun.toString()} readOnly />
|
||||
</div>
|
||||
) : null}
|
||||
{formattedErrorCodeMapping ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Error Code Mapping</h1>
|
||||
</div>
|
||||
<AutoResizingTextarea value={formattedErrorCodeMapping} readOnly />
|
||||
</div>
|
||||
) : null}
|
||||
{!downloadSuffix &&
|
||||
typeof downloadTimeout !== "number" &&
|
||||
typeof maxRetries !== "number" &&
|
||||
typeof maxStepsPerRun !== "number" &&
|
||||
!formattedErrorCodeMapping ? (
|
||||
<div className="text-sm text-slate-400">
|
||||
No additional download-specific metadata configured for this block.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { FileDownloadBlockParameters };
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
type Props = {
|
||||
url: string;
|
||||
continueOnFailure: boolean;
|
||||
};
|
||||
|
||||
function GotoUrlBlockParameters({ url, continueOnFailure }: Props) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">URL</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
The destination Skyvern navigates to
|
||||
</h2>
|
||||
</div>
|
||||
<Input value={url} readOnly />
|
||||
</div>
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Continue on Failure</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Whether to continue if navigation fails
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<Switch checked={continueOnFailure} disabled />
|
||||
<span className="text-sm text-slate-400">
|
||||
{continueOnFailure ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GotoUrlBlockParameters };
|
||||
@@ -0,0 +1,86 @@
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import type { WorkflowParameter } from "@/routes/workflows/types/workflowTypes";
|
||||
|
||||
type Props = {
|
||||
prompt: string;
|
||||
llmKey?: string | null;
|
||||
jsonSchema?: Record<string, unknown> | string | null;
|
||||
parameters?: Array<WorkflowParameter>;
|
||||
};
|
||||
|
||||
function TextPromptBlockParameters({
|
||||
prompt,
|
||||
llmKey,
|
||||
jsonSchema,
|
||||
parameters,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Prompt</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Instructions passed to the selected LLM
|
||||
</h2>
|
||||
</div>
|
||||
<AutoResizingTextarea value={prompt} readOnly />
|
||||
</div>
|
||||
{llmKey ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">LLM Key</h1>
|
||||
</div>
|
||||
<Input value={llmKey} readOnly />
|
||||
</div>
|
||||
) : null}
|
||||
{jsonSchema ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">JSON Schema</h1>
|
||||
<h2 className="text-base text-slate-400">
|
||||
Expected shape of the model response
|
||||
</h2>
|
||||
</div>
|
||||
<CodeEditor
|
||||
className="w-full"
|
||||
language="json"
|
||||
value={
|
||||
typeof jsonSchema === "string"
|
||||
? jsonSchema
|
||||
: JSON.stringify(jsonSchema, null, 2)
|
||||
}
|
||||
readOnly
|
||||
minHeight="160px"
|
||||
maxHeight="400px"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{parameters && parameters.length > 0 ? (
|
||||
<div className="flex gap-16">
|
||||
<div className="w-80">
|
||||
<h1 className="text-lg">Parameters</h1>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
{parameters.map((parameter) => (
|
||||
<div
|
||||
key={parameter.key}
|
||||
className="rounded border border-slate-700/40 bg-slate-elevation3 p-3"
|
||||
>
|
||||
<p className="font-medium">{parameter.key}</p>
|
||||
{parameter.description ? (
|
||||
<p className="text-sm text-slate-400">
|
||||
{parameter.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TextPromptBlockParameters };
|
||||
Reference in New Issue
Block a user