Merge Navigation V1 and Task V2 blocks into unified Browser Task block (#4695)
Co-authored-by: Suchintan Singh <suchintan@skyvern.com>
This commit is contained in:
@@ -6,23 +6,100 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "./ui/select";
|
} from "./ui/select";
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
|
type EngineOption = {
|
||||||
|
value: RunEngine;
|
||||||
|
label: string;
|
||||||
|
badge?: string;
|
||||||
|
badgeVariant?: "default" | "success" | "warning";
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: RunEngine | null;
|
value: RunEngine | null;
|
||||||
onChange: (value: RunEngine) => void;
|
onChange: (value: RunEngine) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
availableEngines?: Array<RunEngine>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function RunEngineSelector({ value, onChange, className }: Props) {
|
const allEngineOptions: Array<EngineOption> = [
|
||||||
|
{
|
||||||
|
value: RunEngine.SkyvernV1,
|
||||||
|
label: "Skyvern 1.0",
|
||||||
|
badge: "Recommended",
|
||||||
|
badgeVariant: "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RunEngine.SkyvernV2,
|
||||||
|
label: "Skyvern 2.0",
|
||||||
|
badge: "Multi-Goal",
|
||||||
|
badgeVariant: "warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RunEngine.OpenaiCua,
|
||||||
|
label: "OpenAI CUA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: RunEngine.AnthropicCua,
|
||||||
|
label: "Anthropic CUA",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Default engines for blocks that don't support V2 mode
|
||||||
|
const defaultEngines: Array<RunEngine> = [
|
||||||
|
RunEngine.SkyvernV1,
|
||||||
|
RunEngine.OpenaiCua,
|
||||||
|
RunEngine.AnthropicCua,
|
||||||
|
];
|
||||||
|
|
||||||
|
function BadgeLabel({ option }: { option: EngineOption }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{option.badge && (
|
||||||
|
<span
|
||||||
|
className={cn("rounded px-1.5 py-0.5 text-[10px] font-medium", {
|
||||||
|
"bg-green-500/20 text-green-400": option.badgeVariant === "success",
|
||||||
|
"bg-amber-500/20 text-amber-400": option.badgeVariant === "warning",
|
||||||
|
"bg-slate-500/20 text-slate-400":
|
||||||
|
option.badgeVariant === "default" || !option.badgeVariant,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{option.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RunEngineSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
availableEngines,
|
||||||
|
}: Props) {
|
||||||
|
const engines = availableEngines ?? defaultEngines;
|
||||||
|
const engineOptions = allEngineOptions.filter((opt) =>
|
||||||
|
engines.includes(opt.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedOption = engineOptions.find(
|
||||||
|
(opt) => opt.value === (value ?? RunEngine.SkyvernV1),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={value ?? RunEngine.SkyvernV1} onValueChange={onChange}>
|
<Select value={value ?? RunEngine.SkyvernV1} onValueChange={onChange}>
|
||||||
<SelectTrigger className={className}>
|
<SelectTrigger className={className}>
|
||||||
<SelectValue />
|
<SelectValue>
|
||||||
|
{selectedOption && <BadgeLabel option={selectedOption} />}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value={RunEngine.SkyvernV1}>Skyvern 1.0</SelectItem>
|
{engineOptions.map((option) => (
|
||||||
<SelectItem value={RunEngine.OpenaiCua}>OpenAI CUA</SelectItem>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
<SelectItem value={RunEngine.AnthropicCua}>Anthropic CUA</SelectItem>
|
<BadgeLabel option={option} />
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export const baseHelpTooltipContent = {
|
|||||||
"When inside a for loop, continue to the next iteration if this block fails.",
|
"When inside a for loop, continue to the next iteration if this block fails.",
|
||||||
includeActionHistoryInVerification:
|
includeActionHistoryInVerification:
|
||||||
"Include the action history in the completion verification.",
|
"Include the action history in the completion verification.",
|
||||||
|
engine:
|
||||||
|
"Skyvern 1.0: Fast, single-goal tasks. Skyvern 2.0: Complex, multi-goal tasks (slower).",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const basePlaceholderContent = {
|
export const basePlaceholderContent = {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useState } from "react";
|
|||||||
import { helpTooltips, placeholders } from "../../helpContent";
|
import { helpTooltips, placeholders } from "../../helpContent";
|
||||||
import { errorMappingExampleValue } from "../types";
|
import { errorMappingExampleValue } from "../types";
|
||||||
import type { NavigationNode } from "./types";
|
import type { NavigationNode } from "./types";
|
||||||
|
import { MAX_STEPS_DEFAULT } from "./types";
|
||||||
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
import { ParametersMultiSelect } from "../TaskNode/ParametersMultiSelect";
|
||||||
import { AppNode } from "..";
|
import { AppNode } from "..";
|
||||||
import {
|
import {
|
||||||
@@ -35,9 +36,11 @@ import { ModelSelector } from "@/components/ModelSelector";
|
|||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { NodeHeader } from "../components/NodeHeader";
|
import { NodeHeader } from "../components/NodeHeader";
|
||||||
|
import { NodeTabs } from "../components/NodeTabs";
|
||||||
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
|
||||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||||
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
import { useUpdate } from "@/routes/workflows/editor/useUpdate";
|
||||||
|
import { RunEngine } from "@/api/types";
|
||||||
|
|
||||||
import { DisableCache } from "../DisableCache";
|
import { DisableCache } from "../DisableCache";
|
||||||
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
import { BlockExecutionOptions } from "../components/BlockExecutionOptions";
|
||||||
@@ -64,45 +67,189 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
const update = useUpdate<NavigationNode["data"]>({ id, editable });
|
const update = useUpdate<NavigationNode["data"]>({ id, editable });
|
||||||
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
const isInsideForLoop = isNodeInsideForLoop(nodes, id);
|
||||||
|
|
||||||
|
// Determine if we're in V2 mode (Skyvern 2.0)
|
||||||
|
const isV2Mode = data.engine === RunEngine.SkyvernV2;
|
||||||
|
|
||||||
|
const handleEngineChange = (value: RunEngine) => {
|
||||||
|
const updates: Partial<NavigationNode["data"]> = { engine: value };
|
||||||
|
if (value === RunEngine.SkyvernV2) {
|
||||||
|
// Switching to V2 — clear V1-specific fields
|
||||||
|
updates.navigationGoal = "";
|
||||||
|
updates.completeCriterion = "";
|
||||||
|
updates.terminateCriterion = "";
|
||||||
|
updates.errorCodeMapping = "null";
|
||||||
|
updates.parameterKeys = [];
|
||||||
|
updates.maxRetries = null;
|
||||||
|
updates.maxStepsOverride = null;
|
||||||
|
updates.allowDownloads = false;
|
||||||
|
updates.downloadSuffix = null;
|
||||||
|
updates.includeActionHistoryInVerification = false;
|
||||||
|
} else if (data.engine === RunEngine.SkyvernV2) {
|
||||||
|
// Switching away from V2 — clear V2-specific fields
|
||||||
|
updates.prompt = "";
|
||||||
|
updates.maxSteps = MAX_STEPS_DEFAULT;
|
||||||
|
}
|
||||||
|
update(updates);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFacing(data.showCode ? "back" : "front");
|
setFacing(data.showCode ? "back" : "front");
|
||||||
}, [data.showCode]);
|
}, [data.showCode]);
|
||||||
|
|
||||||
return (
|
// V2 Mode UI (simpler interface)
|
||||||
<Flippable facing={facing} preserveFrontsideHeight={true}>
|
const renderV2Content = () => (
|
||||||
<div>
|
<>
|
||||||
<Handle
|
|
||||||
type="source"
|
|
||||||
position={Position.Bottom}
|
|
||||||
id="a"
|
|
||||||
className="opacity-0"
|
|
||||||
/>
|
|
||||||
<Handle
|
|
||||||
type="target"
|
|
||||||
position={Position.Top}
|
|
||||||
id="b"
|
|
||||||
className="opacity-0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("space-y-4", {
|
||||||
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
"opacity-50": thisBlockIsPlaying,
|
||||||
{
|
})}
|
||||||
"pointer-events-none": thisBlockIsPlaying,
|
|
||||||
"bg-slate-950 outline outline-2 outline-slate-300":
|
|
||||||
thisBlockIsTargetted,
|
|
||||||
},
|
|
||||||
data.comparisonColor,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<NodeHeader
|
<div className="space-y-2">
|
||||||
blockLabel={label}
|
<div className="flex gap-2">
|
||||||
editable={editable}
|
<Label className="text-xs text-slate-300">URL</Label>
|
||||||
|
<HelpTooltip content={helpTooltips["navigation"]["url"]} />
|
||||||
|
</div>
|
||||||
|
<WorkflowBlockInputTextarea
|
||||||
nodeId={id}
|
nodeId={id}
|
||||||
totpIdentifier={data.totpIdentifier}
|
onChange={(value) => {
|
||||||
totpUrl={data.totpVerificationUrl}
|
update({ url: value });
|
||||||
type={type}
|
}}
|
||||||
|
value={data.url}
|
||||||
|
placeholder={placeholders["taskv2"]["url"]}
|
||||||
|
className="nopan text-xs"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Label className="text-xs text-slate-300">Prompt</Label>
|
||||||
|
{isFirstWorkflowBlock ? (
|
||||||
|
<div className="flex justify-end text-xs text-slate-400">
|
||||||
|
Tip: Use the {"+"} button to add parameters!
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<WorkflowBlockInputTextarea
|
||||||
|
aiImprove={AI_IMPROVE_CONFIGS.taskV2.prompt}
|
||||||
|
nodeId={id}
|
||||||
|
onChange={(value) => {
|
||||||
|
update({ prompt: value });
|
||||||
|
}}
|
||||||
|
value={data.prompt}
|
||||||
|
placeholder={placeholders["taskv2"]["prompt"]}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">Engine</Label>
|
||||||
|
<HelpTooltip content={helpTooltips["navigation"]["engine"]} />
|
||||||
|
</div>
|
||||||
|
<RunEngineSelector
|
||||||
|
value={data.engine}
|
||||||
|
onChange={handleEngineChange}
|
||||||
|
className="nopan w-72 text-xs"
|
||||||
|
availableEngines={[
|
||||||
|
RunEngine.SkyvernV1,
|
||||||
|
RunEngine.SkyvernV2,
|
||||||
|
RunEngine.OpenaiCua,
|
||||||
|
RunEngine.AnthropicCua,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<Accordion
|
||||||
|
type="single"
|
||||||
|
collapsible
|
||||||
|
onValueChange={() => rerender.bump()}
|
||||||
|
>
|
||||||
|
<AccordionItem value="advanced" className="border-b-0">
|
||||||
|
<AccordionTrigger className="py-0">
|
||||||
|
Advanced Settings
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent key={rerender.key} className="pl-6 pr-1 pt-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<ModelSelector
|
||||||
|
className="nopan w-52 text-xs"
|
||||||
|
value={data.model}
|
||||||
|
onChange={(value) => {
|
||||||
|
update({ model: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">Max Steps</Label>
|
||||||
|
<HelpTooltip content={helpTooltips["taskv2"]["maxSteps"]} />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={`${MAX_STEPS_DEFAULT}`}
|
||||||
|
className="nopan text-xs"
|
||||||
|
value={data.maxSteps ?? MAX_STEPS_DEFAULT}
|
||||||
|
onChange={(event) => {
|
||||||
|
update({
|
||||||
|
maxSteps: Number(event.target.value),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<DisableCache
|
||||||
|
disableCache={data.disableCache}
|
||||||
|
editable={editable}
|
||||||
|
onDisableCacheChange={(disableCache) => {
|
||||||
|
update({ disableCache });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">
|
||||||
|
2FA Identifier
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={helpTooltips["taskv2"]["totpIdentifier"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<WorkflowBlockInputTextarea
|
||||||
|
nodeId={id}
|
||||||
|
onChange={(value) => {
|
||||||
|
update({ totpIdentifier: value });
|
||||||
|
}}
|
||||||
|
value={data.totpIdentifier ?? ""}
|
||||||
|
placeholder={placeholders["navigation"]["totpIdentifier"]}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs text-slate-300">
|
||||||
|
2FA Verification URL
|
||||||
|
</Label>
|
||||||
|
<HelpTooltip
|
||||||
|
content={helpTooltips["task"]["totpVerificationUrl"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<WorkflowBlockInputTextarea
|
||||||
|
nodeId={id}
|
||||||
|
onChange={(value) => {
|
||||||
|
update({ totpVerificationUrl: value });
|
||||||
|
}}
|
||||||
|
value={data.totpVerificationUrl ?? ""}
|
||||||
|
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
// V1 Mode UI (full navigation interface)
|
||||||
|
const renderV1Content = () => (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn("space-y-4", {
|
className={cn("space-y-4", {
|
||||||
"opacity-50": thisBlockIsPlaying,
|
"opacity-50": thisBlockIsPlaying,
|
||||||
@@ -133,9 +280,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Label className="text-xs text-slate-300">
|
<Label className="text-xs text-slate-300">Prompt</Label>
|
||||||
Navigation Goal
|
|
||||||
</Label>
|
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
content={helpTooltips["navigation"]["navigationGoal"]}
|
content={helpTooltips["navigation"]["navigationGoal"]}
|
||||||
/>
|
/>
|
||||||
@@ -153,13 +298,30 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="rounded-md bg-slate-800 p-2">
|
<div className="rounded-md bg-slate-800 p-2">
|
||||||
<div className="space-y-1 text-xs text-slate-400">
|
<div className="space-y-1 text-xs text-slate-400">
|
||||||
Tip: Try to phrase your prompt as a goal with an explicit
|
Tip: Try to phrase your prompt as a goal with an explicit completion
|
||||||
completion criteria. While executing, Skyvern will take as many
|
criteria. While executing, Skyvern will take as many actions as
|
||||||
actions as necessary to accomplish the goal. Use words like
|
necessary to accomplish the goal. Use words like "Complete" or
|
||||||
"Complete" or "Terminate" to help Skyvern identify when it's
|
"Terminate" to help Skyvern identify when it's finished or when it
|
||||||
finished or when it should give up.
|
should give up.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-xs font-normal text-slate-300">Engine</Label>
|
||||||
|
<HelpTooltip content={helpTooltips["navigation"]["engine"]} />
|
||||||
|
</div>
|
||||||
|
<RunEngineSelector
|
||||||
|
value={data.engine}
|
||||||
|
onChange={handleEngineChange}
|
||||||
|
className="nopan w-72 text-xs"
|
||||||
|
availableEngines={[
|
||||||
|
RunEngine.SkyvernV1,
|
||||||
|
RunEngine.SkyvernV2,
|
||||||
|
RunEngine.OpenaiCua,
|
||||||
|
RunEngine.AnthropicCua,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Accordion
|
<Accordion
|
||||||
@@ -186,13 +348,9 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs text-slate-300">
|
<Label className="text-xs text-slate-300">Complete if...</Label>
|
||||||
Complete if...
|
|
||||||
</Label>
|
|
||||||
<WorkflowBlockInputTextarea
|
<WorkflowBlockInputTextarea
|
||||||
aiImprove={
|
aiImprove={AI_IMPROVE_CONFIGS.navigation.completeCriterion}
|
||||||
AI_IMPROVE_CONFIGS.navigation.completeCriterion
|
|
||||||
}
|
|
||||||
nodeId={id}
|
nodeId={id}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
update({ completeCriterion: value });
|
update({ completeCriterion: value });
|
||||||
@@ -209,20 +367,6 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
update({ model: value });
|
update({ model: value });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
|
||||||
Engine
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<RunEngineSelector
|
|
||||||
value={data.engine}
|
|
||||||
onChange={(value) => {
|
|
||||||
update({ engine: value });
|
|
||||||
}}
|
|
||||||
className="nopan w-52 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
@@ -234,9 +378,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={
|
placeholder={placeholders["navigation"]["maxStepsOverride"]}
|
||||||
placeholders["navigation"]["maxStepsOverride"]
|
|
||||||
}
|
|
||||||
className="nopan w-52 text-xs"
|
className="nopan w-52 text-xs"
|
||||||
min="0"
|
min="0"
|
||||||
value={data.maxStepsOverride ?? ""}
|
value={data.maxStepsOverride ?? ""}
|
||||||
@@ -256,9 +398,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
Error Messages
|
Error Messages
|
||||||
</Label>
|
</Label>
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
content={
|
content={helpTooltips["navigation"]["errorCodeMapping"]}
|
||||||
helpTooltips["navigation"]["errorCodeMapping"]
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -267,11 +407,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
update({
|
update({
|
||||||
errorCodeMapping: checked
|
errorCodeMapping: checked
|
||||||
? JSON.stringify(
|
? JSON.stringify(errorMappingExampleValue, null, 2)
|
||||||
errorMappingExampleValue,
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
: "null",
|
: "null",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -331,9 +467,7 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
Complete on Download
|
Complete on Download
|
||||||
</Label>
|
</Label>
|
||||||
<HelpTooltip
|
<HelpTooltip
|
||||||
content={
|
content={helpTooltips["navigation"]["completeOnDownload"]}
|
||||||
helpTooltips["navigation"]["completeOnDownload"]
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-52">
|
<div className="w-52">
|
||||||
@@ -408,10 +542,54 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flippable facing={facing} preserveFrontsideHeight={true}>
|
||||||
|
<div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="a"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="b"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"transform-origin-center w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4 transition-all",
|
||||||
|
{
|
||||||
|
"pointer-events-none": thisBlockIsPlaying,
|
||||||
|
"bg-slate-950 outline outline-2 outline-slate-300":
|
||||||
|
thisBlockIsTargetted,
|
||||||
|
},
|
||||||
|
data.comparisonColor,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NodeHeader
|
||||||
|
blockLabel={label}
|
||||||
|
editable={editable}
|
||||||
|
nodeId={id}
|
||||||
|
totpIdentifier={data.totpIdentifier}
|
||||||
|
totpUrl={data.totpVerificationUrl}
|
||||||
|
type={isV2Mode ? "task_v2" : type}
|
||||||
|
/>
|
||||||
|
{isV2Mode ? renderV2Content() : renderV1Content()}
|
||||||
|
<NodeTabs blockLabel={label} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BlockCodeEditor blockLabel={label} blockType={type} script={script} />
|
<BlockCodeEditor
|
||||||
|
blockLabel={label}
|
||||||
|
blockType={isV2Mode ? "task_v2" : type}
|
||||||
|
script={script}
|
||||||
|
/>
|
||||||
</Flippable>
|
</Flippable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { NodeBaseData } from "../types";
|
|||||||
import { RunEngine } from "@/api/types";
|
import { RunEngine } from "@/api/types";
|
||||||
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
import { debuggableWorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes";
|
||||||
|
|
||||||
|
export const MAX_STEPS_DEFAULT = 25;
|
||||||
|
|
||||||
export type NavigationNodeData = NodeBaseData & {
|
export type NavigationNodeData = NodeBaseData & {
|
||||||
url: string;
|
url: string;
|
||||||
navigationGoal: string;
|
navigationGoal: string;
|
||||||
@@ -19,6 +21,9 @@ export type NavigationNodeData = NodeBaseData & {
|
|||||||
totpIdentifier: string | null;
|
totpIdentifier: string | null;
|
||||||
disableCache: boolean;
|
disableCache: boolean;
|
||||||
includeActionHistoryInVerification: boolean;
|
includeActionHistoryInVerification: boolean;
|
||||||
|
// V2-specific fields (used when engine is SkyvernV2)
|
||||||
|
prompt: string;
|
||||||
|
maxSteps: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NavigationNode = Node<NavigationNodeData, "navigation">;
|
export type NavigationNode = Node<NavigationNodeData, "navigation">;
|
||||||
@@ -44,6 +49,9 @@ export const navigationNodeDefaultData: NavigationNodeData = {
|
|||||||
continueOnFailure: false,
|
continueOnFailure: false,
|
||||||
disableCache: false,
|
disableCache: false,
|
||||||
includeActionHistoryInVerification: false,
|
includeActionHistoryInVerification: false,
|
||||||
|
// V2-specific fields
|
||||||
|
prompt: "",
|
||||||
|
maxSteps: MAX_STEPS_DEFAULT,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function isNavigationNode(node: Node): node is NavigationNode {
|
export function isNavigationNode(node: Node): node is NavigationNode {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ type Props = {
|
|||||||
// Mapping from WorkflowBlock.block_type to ReactFlow node.type
|
// Mapping from WorkflowBlock.block_type to ReactFlow node.type
|
||||||
const BLOCK_TYPE_TO_NODE_TYPE: Record<string, string> = {
|
const BLOCK_TYPE_TO_NODE_TYPE: Record<string, string> = {
|
||||||
task: "task",
|
task: "task",
|
||||||
task_v2: "taskv2",
|
task_v2: "navigation", // task_v2 blocks are displayed as navigation nodes with V2 engine
|
||||||
validation: "validation",
|
validation: "validation",
|
||||||
action: "action",
|
action: "action",
|
||||||
navigation: "navigation",
|
navigation: "navigation",
|
||||||
|
|||||||
@@ -43,17 +43,6 @@ const nodeLibraryItems: Array<{
|
|||||||
title: "Browser Task Block",
|
title: "Browser Task Block",
|
||||||
description: "Take actions to achieve a task.",
|
description: "Take actions to achieve a task.",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
nodeType: "taskv2",
|
|
||||||
icon: (
|
|
||||||
<WorkflowBlockIcon
|
|
||||||
workflowBlockType={WorkflowBlockTypes.Taskv2}
|
|
||||||
className="size-6"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
title: "Browser Task v2 Block",
|
|
||||||
description: "Achieve complex tasks with deep thinking.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
nodeType: "action",
|
nodeType: "action",
|
||||||
icon: (
|
icon: (
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ import { actionNodeDefaultData, isActionNode } from "./nodes/ActionNode/types";
|
|||||||
import {
|
import {
|
||||||
isNavigationNode,
|
isNavigationNode,
|
||||||
navigationNodeDefaultData,
|
navigationNodeDefaultData,
|
||||||
|
MAX_STEPS_DEFAULT,
|
||||||
} from "./nodes/NavigationNode/types";
|
} from "./nodes/NavigationNode/types";
|
||||||
import {
|
import {
|
||||||
extractionNodeDefaultData,
|
extractionNodeDefaultData,
|
||||||
@@ -119,7 +120,6 @@ import {
|
|||||||
isPdfParserNode,
|
isPdfParserNode,
|
||||||
pdfParserNodeDefaultData,
|
pdfParserNodeDefaultData,
|
||||||
} from "./nodes/PDFParserNode/types";
|
} from "./nodes/PDFParserNode/types";
|
||||||
import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types";
|
|
||||||
import { urlNodeDefaultData } from "./nodes/URLNode/types";
|
import { urlNodeDefaultData } from "./nodes/URLNode/types";
|
||||||
import { fileUploadNodeDefaultData } from "./nodes/FileUploadNode/types";
|
import { fileUploadNodeDefaultData } from "./nodes/FileUploadNode/types";
|
||||||
import {
|
import {
|
||||||
@@ -527,19 +527,33 @@ function convertToNode(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "task_v2": {
|
case "task_v2": {
|
||||||
|
// Convert task_v2 blocks to navigation nodes with engine=SkyvernV2
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
...common,
|
...common,
|
||||||
type: "taskv2",
|
type: "navigation",
|
||||||
data: {
|
data: {
|
||||||
...commonData,
|
...commonData,
|
||||||
|
// V2-specific fields
|
||||||
prompt: block.prompt,
|
prompt: block.prompt,
|
||||||
url: block.url ?? "",
|
url: block.url ?? "",
|
||||||
maxSteps: block.max_steps,
|
maxSteps: block.max_steps ?? MAX_STEPS_DEFAULT,
|
||||||
disableCache: block.disable_cache ?? false,
|
disableCache: block.disable_cache ?? false,
|
||||||
totpIdentifier: block.totp_identifier,
|
totpIdentifier: block.totp_identifier,
|
||||||
totpVerificationUrl: block.totp_verification_url,
|
totpVerificationUrl: block.totp_verification_url,
|
||||||
maxScreenshotScrolls: null,
|
// Set engine to SkyvernV2 to indicate V2 mode
|
||||||
|
engine: RunEngine.SkyvernV2,
|
||||||
|
// Default V1 fields (not used in V2 mode but needed for type compatibility)
|
||||||
|
navigationGoal: "",
|
||||||
|
errorCodeMapping: "null",
|
||||||
|
completeCriterion: "",
|
||||||
|
terminateCriterion: "",
|
||||||
|
maxRetries: null,
|
||||||
|
maxStepsOverride: null,
|
||||||
|
allowDownloads: false,
|
||||||
|
downloadSuffix: null,
|
||||||
|
parameterKeys: [],
|
||||||
|
includeActionHistoryInVerification: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -602,6 +616,8 @@ function convertToNode(
|
|||||||
engine: block.engine ?? RunEngine.SkyvernV1,
|
engine: block.engine ?? RunEngine.SkyvernV1,
|
||||||
includeActionHistoryInVerification:
|
includeActionHistoryInVerification:
|
||||||
block.include_action_history_in_verification ?? false,
|
block.include_action_history_in_verification ?? false,
|
||||||
|
prompt: "",
|
||||||
|
maxSteps: MAX_STEPS_DEFAULT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1699,13 +1715,15 @@ function createNode(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "taskv2": {
|
case "taskv2": {
|
||||||
|
// Redirect taskv2 creation to navigation with SkyvernV2 engine
|
||||||
return {
|
return {
|
||||||
...identifiers,
|
...identifiers,
|
||||||
...common,
|
...common,
|
||||||
type: "taskv2",
|
type: "navigation",
|
||||||
data: {
|
data: {
|
||||||
...taskv2NodeDefaultData,
|
...navigationNodeDefaultData,
|
||||||
label,
|
label,
|
||||||
|
engine: RunEngine.SkyvernV2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2164,6 +2182,20 @@ function getWorkflowBlock(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "navigation": {
|
case "navigation": {
|
||||||
|
// If engine is SkyvernV2, convert to task_v2 block
|
||||||
|
if (node.data.engine === RunEngine.SkyvernV2) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
block_type: "task_v2",
|
||||||
|
prompt: node.data.prompt,
|
||||||
|
max_steps: node.data.maxSteps,
|
||||||
|
totp_identifier: node.data.totpIdentifier,
|
||||||
|
totp_verification_url: node.data.totpVerificationUrl,
|
||||||
|
url: node.data.url,
|
||||||
|
disable_cache: node.data.disableCache ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Otherwise, create a navigation block
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
block_type: "navigation",
|
block_type: "navigation",
|
||||||
@@ -3931,8 +3963,15 @@ function getWorkflowErrors(nodes: Array<AppNode>): Array<string> {
|
|||||||
|
|
||||||
const navigationNodes = nodes.filter(isNavigationNode);
|
const navigationNodes = nodes.filter(isNavigationNode);
|
||||||
navigationNodes.forEach((node) => {
|
navigationNodes.forEach((node) => {
|
||||||
if (node.data.navigationGoal.length === 0) {
|
// V2 mode uses prompt, V1 mode uses navigationGoal
|
||||||
errors.push(`${node.data.label}: Navigation goal is required.`);
|
if (node.data.engine === RunEngine.SkyvernV2) {
|
||||||
|
if (!node.data.prompt || node.data.prompt.length === 0) {
|
||||||
|
errors.push(`${node.data.label}: Prompt is required.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!node.data.navigationGoal || node.data.navigationGoal.length === 0) {
|
||||||
|
errors.push(`${node.data.label}: Prompt is required.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user