show metadata for non-navigation blocks on workflow parameters page (#4243)

This commit is contained in:
Celal Zamanoglu
2025-12-10 00:07:43 +03:00
committed by GitHub
parent c939d34603
commit 9fa7c0c96e
5 changed files with 407 additions and 16 deletions

View File

@@ -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;
}

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 };