Jon/sky 5820 make browser task block flippable with code (#3165)
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { ExitIcon } from "@radix-ui/react-icons";
|
||||
import { Handle } from "@xyflow/react";
|
||||
import { Position } from "@xyflow/react";
|
||||
|
||||
import { WorkflowBlockType } from "@/routes/workflows/types/workflowTypes";
|
||||
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
import { CodeEditor } from "./CodeEditor";
|
||||
import { workflowBlockTitle } from "../editor/nodes/types";
|
||||
import { WorkflowBlockIcon } from "../editor/nodes/WorkflowBlockIcon";
|
||||
|
||||
function BlockCodeEditor({
|
||||
blockLabel,
|
||||
blockType,
|
||||
script,
|
||||
onClick,
|
||||
}: {
|
||||
blockLabel: string;
|
||||
blockType: WorkflowBlockType;
|
||||
script: string | undefined;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
}) {
|
||||
const blockTitle = workflowBlockTitle[blockType];
|
||||
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<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 flex h-full w-[30rem] flex-col space-y-4 rounded-lg border border-slate-600 bg-slate-elevation3 px-6 py-4 transition-all",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
onClick?.(e);
|
||||
}}
|
||||
>
|
||||
<header className="!mt-0 flex h-[2.75rem] justify-between gap-2">
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="relative flex h-[2.75rem] w-[2.75rem] items-center justify-center overflow-hidden rounded border border-slate-600">
|
||||
<WorkflowBlockIcon
|
||||
workflowBlockType={blockType}
|
||||
className="size-6"
|
||||
/>
|
||||
<div className="absolute -left-3 top-8 flex h-4 w-16 origin-top-left -rotate-45 transform items-center justify-center bg-yellow-400">
|
||||
<span className="text-xs font-bold text-black">code</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{blockLabel}
|
||||
<span className="text-xs text-slate-400">{blockTitle}</span>
|
||||
</div>
|
||||
<div className="ml-auto flex w-[2.75rem] items-center justify-center rounded hover:bg-slate-800">
|
||||
<ExitIcon
|
||||
onClick={() => {
|
||||
toggleScriptForNodeCallback({
|
||||
label: blockLabel,
|
||||
show: false,
|
||||
});
|
||||
}}
|
||||
className="size-5 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{script ? (
|
||||
<div className="h-full flex-1 overflow-y-hidden">
|
||||
<CodeEditor
|
||||
key="static"
|
||||
className="nopan nowheel h-full overflow-y-scroll"
|
||||
language="python"
|
||||
value={script}
|
||||
lineWrap={false}
|
||||
readOnly
|
||||
fontSize={10}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-slate-950">
|
||||
No script defined
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BlockCodeEditor };
|
||||
@@ -20,6 +20,7 @@ type Props = {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
language?: "python" | "json" | "html";
|
||||
lineWrap?: boolean;
|
||||
readOnly?: boolean;
|
||||
minHeight?: string;
|
||||
maxHeight?: string;
|
||||
@@ -33,13 +34,14 @@ function CodeEditor({
|
||||
minHeight,
|
||||
maxHeight,
|
||||
language,
|
||||
lineWrap = true,
|
||||
className,
|
||||
readOnly = false,
|
||||
fontSize = 12,
|
||||
}: Props) {
|
||||
const extensions = language
|
||||
? [getLanguageExtension(language), EditorView.lineWrapping]
|
||||
: [EditorView.lineWrapping];
|
||||
? [getLanguageExtension(language), lineWrap ? EditorView.lineWrapping : []]
|
||||
: [lineWrap ? EditorView.lineWrapping : []];
|
||||
|
||||
return (
|
||||
<CodeMirror
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { useOnChange } from "@/hooks/useOnChange";
|
||||
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
||||
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
||||
import { BlockActionContext } from "@/store/BlockActionContext";
|
||||
import { useDebugStore } from "@/store/useDebugStore";
|
||||
import {
|
||||
useWorkflowHasChangesStore,
|
||||
@@ -543,6 +543,37 @@ function FlowRenderer({
|
||||
doLayout(newNodesWithUpdatedParameters, newEdges);
|
||||
}
|
||||
|
||||
function toggleScript({
|
||||
id,
|
||||
label,
|
||||
show,
|
||||
}: {
|
||||
id?: string;
|
||||
label?: string;
|
||||
show: boolean;
|
||||
}) {
|
||||
if (id) {
|
||||
const node = nodes.find((node) => node.id === id);
|
||||
if (!node || !isWorkflowBlockNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.data.showCode = show;
|
||||
} else if (label) {
|
||||
const node = nodes.find(
|
||||
(node) => "label" in node.data && node.data.label === label,
|
||||
);
|
||||
|
||||
if (!node || !isWorkflowBlockNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.data.showCode = show;
|
||||
}
|
||||
|
||||
doLayout(nodes, edges);
|
||||
}
|
||||
|
||||
const editorElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useAutoPan(editorElementRef, nodes);
|
||||
@@ -642,7 +673,12 @@ function FlowRenderer({
|
||||
<WorkflowParametersStateContext.Provider
|
||||
value={[parameters, setParameters]}
|
||||
>
|
||||
<DeleteNodeCallbackContext.Provider value={deleteNode}>
|
||||
<BlockActionContext.Provider
|
||||
value={{
|
||||
deleteNodeCallback: deleteNode,
|
||||
toggleScriptForNodeCallback: toggleScript,
|
||||
}}
|
||||
>
|
||||
<ReactFlow
|
||||
ref={editorElementRef}
|
||||
nodes={nodes}
|
||||
@@ -772,7 +808,7 @@ function FlowRenderer({
|
||||
</Panel>
|
||||
)}
|
||||
</ReactFlow>
|
||||
</DeleteNodeCallbackContext.Provider>
|
||||
</BlockActionContext.Provider>
|
||||
</WorkflowParametersStateContext.Provider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,8 @@ import { toast } from "@/components/ui/use-toast";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||
import { statusIsFinalized } from "@/routes/tasks/types.ts";
|
||||
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import { useSidebarStore } from "@/store/SidebarStore";
|
||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
@@ -47,12 +49,18 @@ function WorkflowDebugger() {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const queryClient = useQueryClient();
|
||||
const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false);
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
|
||||
const { data: workflow } = useWorkflowQuery({
|
||||
workflowPermanentId,
|
||||
});
|
||||
|
||||
const { data: blockScripts } = useBlockScriptsQuery({
|
||||
workflowPermanentId,
|
||||
});
|
||||
|
||||
const { data: debugSession } = useDebugSessionQuery({
|
||||
workflowPermanentId,
|
||||
enabled: shouldFetchDebugSession && !!workflowPermanentId,
|
||||
@@ -80,6 +88,11 @@ function WorkflowDebugger() {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
blockScriptStore.setScripts(blockScripts ?? {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockScripts]);
|
||||
|
||||
const afterCycleBrowser = () => {
|
||||
setOpenDialogue(false);
|
||||
setShowPowerButton(false);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||
import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import { useSidebarStore } from "@/store/SidebarStore";
|
||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
@@ -17,6 +20,7 @@ function WorkflowEditor() {
|
||||
return state.setCollapsed;
|
||||
});
|
||||
const workflowChangesStore = useWorkflowHasChangesStore();
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
|
||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
||||
workflowPermanentId,
|
||||
@@ -25,11 +29,20 @@ function WorkflowEditor() {
|
||||
const { data: globalWorkflows, isLoading: isGlobalWorkflowsLoading } =
|
||||
useGlobalWorkflowsQuery();
|
||||
|
||||
const { data: blockScripts } = useBlockScriptsQuery({
|
||||
workflowPermanentId,
|
||||
});
|
||||
|
||||
useMountEffect(() => {
|
||||
setCollapsed(true);
|
||||
workflowChangesStore.setHasChanges(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
blockScriptStore.setScripts(blockScripts ?? {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockScripts]);
|
||||
|
||||
if (isLoading || isGlobalWorkflowsLoading) {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { Flippable } from "@/components/Flippable";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import {
|
||||
Accordion,
|
||||
@@ -12,7 +14,9 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { WorkflowBlockInput } from "@/components/WorkflowBlockInput";
|
||||
import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea";
|
||||
import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { useBlockScriptStore } from "@/store/BlockScriptStore";
|
||||
import {
|
||||
Handle,
|
||||
NodeProps,
|
||||
@@ -40,7 +44,10 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
const { blockLabel: urlBlockLabel } = useParams();
|
||||
const debugStore = useDebugStore();
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const [facing, setFacing] = useState<"front" | "back">("front");
|
||||
const blockScriptStore = useBlockScriptStore();
|
||||
const { editable, debuggable, label } = data;
|
||||
const script = blockScriptStore.scripts[label];
|
||||
const thisBlockIsPlaying =
|
||||
urlBlockLabel !== undefined && urlBlockLabel === label;
|
||||
const elideFromDebugging = debugStore.isDebugMode && !debuggable;
|
||||
@@ -76,359 +83,382 @@ function NavigationNode({ id, data, type }: NodeProps<NavigationNode>) {
|
||||
updateNodeData(id, { [key]: value });
|
||||
}
|
||||
|
||||
return (
|
||||
<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 bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
disabled={elideFromDebugging}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<div
|
||||
className={cn("space-y-4", {
|
||||
"opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">URL</Label>
|
||||
<HelpTooltip content={helpTooltips["navigation"]["url"]} />
|
||||
</div>
|
||||
{isFirstWorkflowBlock ? (
|
||||
<div className="flex justify-end text-xs text-slate-400">
|
||||
Tip: Use the {"+"} button to add parameters!
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
useEffect(() => {
|
||||
setFacing(data.showCode ? "back" : "front");
|
||||
}, [data.showCode]);
|
||||
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
}}
|
||||
value={inputs.url}
|
||||
placeholder={placeholders["navigation"]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">Prompt</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["navigationGoal"]}
|
||||
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 bg-slate-950 outline outline-2 outline-slate-300":
|
||||
thisBlockIsPlaying,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<NodeHeader
|
||||
blockLabel={label}
|
||||
editable={editable}
|
||||
disabled={elideFromDebugging}
|
||||
nodeId={id}
|
||||
totpIdentifier={inputs.totpIdentifier}
|
||||
totpUrl={inputs.totpVerificationUrl}
|
||||
type={type}
|
||||
/>
|
||||
<div
|
||||
className={cn("space-y-4", {
|
||||
"opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">URL</Label>
|
||||
<HelpTooltip content={helpTooltips["navigation"]["url"]} />
|
||||
</div>
|
||||
{isFirstWorkflowBlock ? (
|
||||
<div className="flex justify-end text-xs text-slate-400">
|
||||
Tip: Use the {"+"} button to add parameters!
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<WorkflowBlockInputTextarea
|
||||
canWriteTitle={true}
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("url", value);
|
||||
}}
|
||||
value={inputs.url}
|
||||
placeholder={placeholders["navigation"]["url"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("navigationGoal", value);
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
placeholder={placeholders["navigation"]["navigationGoal"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-md bg-slate-800 p-2">
|
||||
<div className="space-y-1 text-xs text-slate-400">
|
||||
Tip: Try to phrase your prompt as a goal with an explicit
|
||||
completion criteria. While executing, Skyvern will take as many
|
||||
actions as necessary to accomplish the goal. Use words like
|
||||
"Complete" or "Terminate" to help Skyvern identify when it's
|
||||
finished or when it should give up.
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">
|
||||
Navigation Goal
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["navigationGoal"]}
|
||||
/>
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("navigationGoal", value);
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
placeholder={placeholders["navigation"]["navigationGoal"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-md bg-slate-800 p-2">
|
||||
<div className="space-y-1 text-xs text-slate-400">
|
||||
Tip: Try to phrase your prompt as a goal with an explicit
|
||||
completion criteria. While executing, Skyvern will take as many
|
||||
actions as necessary to accomplish the goal. Use words like
|
||||
"Complete" or "Terminate" to help Skyvern identify when it's
|
||||
finished or when it should give up.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<Accordion
|
||||
className={cn({
|
||||
"pointer-events-none opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
type="single"
|
||||
collapsible
|
||||
>
|
||||
<AccordionItem value="advanced" className="border-b-0">
|
||||
<AccordionTrigger className="py-0">
|
||||
Advanced Settings
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pl-6 pr-1 pt-1">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<ParametersMultiSelect
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-slate-300">
|
||||
Complete if...
|
||||
</Label>
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("completeCriterion", value);
|
||||
}}
|
||||
value={inputs.completeCriterion}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.model}
|
||||
onChange={(value) => {
|
||||
handleChange("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={inputs.engine}
|
||||
onChange={(value) => {
|
||||
handleChange("engine", value);
|
||||
}}
|
||||
className="nopan w-52 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Max Steps Override
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["maxStepsOverride"]}
|
||||
<Separator />
|
||||
<Accordion
|
||||
className={cn({
|
||||
"pointer-events-none opacity-50": thisBlockIsPlaying,
|
||||
})}
|
||||
type="single"
|
||||
collapsible
|
||||
>
|
||||
<AccordionItem value="advanced" className="border-b-0">
|
||||
<AccordionTrigger className="py-0">
|
||||
Advanced Settings
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pl-6 pr-1 pt-1">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<ParametersMultiSelect
|
||||
availableOutputParameters={outputParameterKeys}
|
||||
parameters={data.parameterKeys}
|
||||
onParametersChange={(parameterKeys) => {
|
||||
updateNodeData(id, { parameterKeys });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={placeholders["navigation"]["maxStepsOverride"]}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-slate-300">
|
||||
Complete if...
|
||||
</Label>
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("completeCriterion", value);
|
||||
}}
|
||||
value={inputs.completeCriterion}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<ModelSelector
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxStepsOverride ?? ""}
|
||||
onChange={(event) => {
|
||||
const value =
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxStepsOverride", value);
|
||||
value={inputs.model}
|
||||
onChange={(value) => {
|
||||
handleChange("model", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
Engine
|
||||
</Label>
|
||||
</div>
|
||||
<RunEngineSelector
|
||||
value={inputs.engine}
|
||||
onChange={(value) => {
|
||||
handleChange("engine", value);
|
||||
}}
|
||||
className="nopan w-52 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Max Steps Override
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["errorCodeMapping"]}
|
||||
content={helpTooltips["navigation"]["maxStepsOverride"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
? JSON.stringify(errorMappingExampleValue, null, 2)
|
||||
: "null",
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
handleChange("errorCodeMapping", value);
|
||||
}}
|
||||
className="nowheel nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Include Action History
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["navigation"][
|
||||
"includeActionHistoryInVerification"
|
||||
]
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={
|
||||
placeholders["navigation"]["maxStepsOverride"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.includeActionHistoryInVerification}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"includeActionHistoryInVerification",
|
||||
checked,
|
||||
);
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxStepsOverride ?? ""}
|
||||
onChange={(event) => {
|
||||
const value =
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxStepsOverride", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["continueOnFailure"]}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["navigation"]["errorCodeMapping"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
? JSON.stringify(
|
||||
errorMappingExampleValue,
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: "null",
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
handleChange("errorCodeMapping", value);
|
||||
}}
|
||||
className="nowheel nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("continueOnFailure", checked);
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Include Action History
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["navigation"][
|
||||
"includeActionHistoryInVerification"
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.includeActionHistoryInVerification}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange(
|
||||
"includeActionHistoryInVerification",
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["navigation"]["continueOnFailure"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("continueOnFailure", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Cache Actions
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["cacheActions"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.cacheActions}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("cacheActions", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Complete on Download
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={
|
||||
helpTooltips["navigation"]["completeOnDownload"]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.allowDownloads}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("allowDownloads", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
File Suffix
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["fileSuffix"]}
|
||||
/>
|
||||
</div>
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
type="text"
|
||||
placeholder={placeholders["navigation"]["downloadSuffix"]}
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
onChange={(value) => {
|
||||
handleChange("downloadSuffix", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Cache Actions
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["cacheActions"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.cacheActions}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("cacheActions", checked);
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">
|
||||
2FA Identifier
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["totpIdentifier"]}
|
||||
/>
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpIdentifier", value);
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
placeholder={placeholders["navigation"]["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Complete on Download
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["completeOnDownload"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.allowDownloads}
|
||||
onCheckedChange={(checked) => {
|
||||
handleChange("allowDownloads", checked);
|
||||
<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) => {
|
||||
handleChange("totpVerificationUrl", value);
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
File Suffix
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["fileSuffix"]}
|
||||
/>
|
||||
</div>
|
||||
<WorkflowBlockInput
|
||||
nodeId={id}
|
||||
type="text"
|
||||
placeholder={placeholders["navigation"]["downloadSuffix"]}
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
onChange={(value) => {
|
||||
handleChange("downloadSuffix", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">
|
||||
2FA Identifier
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltips["navigation"]["totpIdentifier"]}
|
||||
/>
|
||||
</div>
|
||||
<WorkflowBlockInputTextarea
|
||||
nodeId={id}
|
||||
onChange={(value) => {
|
||||
handleChange("totpIdentifier", value);
|
||||
}}
|
||||
value={inputs.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) => {
|
||||
handleChange("totpVerificationUrl", value);
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
placeholder={placeholders["task"]["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BlockCodeEditor blockLabel={label} blockType={type} script={script} />
|
||||
</Flippable>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
|
||||
import { OrgWalled } from "@/components/Orgwalled";
|
||||
|
||||
type Props = {
|
||||
onDelete: () => void;
|
||||
onShowScript?: () => void;
|
||||
};
|
||||
|
||||
function NodeActionMenu({ onDelete }: Props) {
|
||||
function NodeActionMenu({ onDelete, onShowScript }: Props) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -28,6 +30,17 @@ function NodeActionMenu({ onDelete }: Props) {
|
||||
>
|
||||
Delete Block
|
||||
</DropdownMenuItem>
|
||||
<OrgWalled className="p-0">
|
||||
{onShowScript && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
onShowScript();
|
||||
}}
|
||||
>
|
||||
Show Script
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgWalled>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useToggleScriptForNodeCallback } from "@/routes/workflows/hooks/useToggleScriptForNodeCallback";
|
||||
import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQuery";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import {
|
||||
@@ -145,6 +146,7 @@ function NodeHeader({
|
||||
});
|
||||
const blockTitle = workflowBlockTitle[type];
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const toggleScriptForNodeCallback = useToggleScriptForNodeCallback();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -411,6 +413,9 @@ function NodeHeader({
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(nodeId);
|
||||
}}
|
||||
onShowScript={() =>
|
||||
toggleScriptForNodeCallback({ id: nodeId, show: true })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ export type NodeBaseData = {
|
||||
continueOnFailure: boolean;
|
||||
editable: boolean;
|
||||
model: WorkflowModel | null;
|
||||
showCode?: boolean;
|
||||
};
|
||||
|
||||
export const errorMappingExampleValue = {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ScriptBlocksResponse } from "../types/scriptTypes";
|
||||
|
||||
type Props = {
|
||||
cacheKey?: string;
|
||||
cacheKeyValue?: string;
|
||||
workflowPermanentId?: string;
|
||||
};
|
||||
|
||||
function useBlockScriptsQuery({
|
||||
cacheKey,
|
||||
cacheKeyValue,
|
||||
workflowPermanentId,
|
||||
}: Props) {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
|
||||
return useQuery<{ [blockName: string]: string }>({
|
||||
queryKey: ["block-scripts", workflowPermanentId, cacheKey, cacheKeyValue],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
|
||||
const result = await client
|
||||
.post<ScriptBlocksResponse>(`/scripts/${workflowPermanentId}/blocks`, {
|
||||
cache_key: cacheKey ?? "",
|
||||
cache_key_value: cacheKeyValue ?? "",
|
||||
})
|
||||
.then((response) => response.data);
|
||||
|
||||
return result.blocks;
|
||||
},
|
||||
enabled: !!workflowPermanentId,
|
||||
});
|
||||
}
|
||||
|
||||
export { useBlockScriptsQuery };
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
||||
import { BlockActionContext } from "@/store/BlockActionContext";
|
||||
import { useContext } from "react";
|
||||
|
||||
function useDeleteNodeCallback() {
|
||||
const deleteNodeCallback = useContext(DeleteNodeCallbackContext);
|
||||
const deleteNodeCallback = useContext(BlockActionContext)?.deleteNodeCallback;
|
||||
|
||||
if (!deleteNodeCallback) {
|
||||
throw new Error(
|
||||
"useDeleteNodeCallback must be used within a DeleteNodeCallbackProvider",
|
||||
"useDeleteNodeCallback must be used within a BlockActionContextProvider",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { BlockActionContext } from "@/store/BlockActionContext";
|
||||
import { useContext } from "react";
|
||||
|
||||
function useToggleScriptForNodeCallback() {
|
||||
const toggleScriptForNodeCallback =
|
||||
useContext(BlockActionContext)?.toggleScriptForNodeCallback;
|
||||
|
||||
if (!toggleScriptForNodeCallback) {
|
||||
throw new Error(
|
||||
"useToggleScriptForNodeCallback must be used within a BlockActionContextProvider",
|
||||
);
|
||||
}
|
||||
|
||||
return toggleScriptForNodeCallback;
|
||||
}
|
||||
|
||||
export { useToggleScriptForNodeCallback };
|
||||
@@ -0,0 +1,3 @@
|
||||
export type ScriptBlocksResponse = {
|
||||
blocks: { [blockName: string]: string };
|
||||
};
|
||||
18
skyvern-frontend/src/store/BlockActionContext.ts
Normal file
18
skyvern-frontend/src/store/BlockActionContext.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
type DeleteNodeCallback = (id: string) => void;
|
||||
type ToggleScriptForNodeCallback = (opts: {
|
||||
id?: string;
|
||||
label?: string;
|
||||
show: boolean;
|
||||
}) => void;
|
||||
|
||||
const BlockActionContext = createContext<
|
||||
| {
|
||||
deleteNodeCallback: DeleteNodeCallback;
|
||||
toggleScriptForNodeCallback?: ToggleScriptForNodeCallback;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
export { BlockActionContext };
|
||||
45
skyvern-frontend/src/store/BlockScriptStore.ts
Normal file
45
skyvern-frontend/src/store/BlockScriptStore.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* A store to hold the scripts for individual blocks in a workflow. As each
|
||||
* workflow has uniquely (and differently) labelled blocks, and those labels
|
||||
* are block identity, we'll eschew strong typing for this, and use a loose
|
||||
* object literal instead.
|
||||
*/
|
||||
|
||||
import { create } from "zustand";
|
||||
|
||||
interface BlockScriptStore {
|
||||
scriptId?: string;
|
||||
scripts: { [k: string]: string };
|
||||
// --
|
||||
setScript: (blockId: string, script: string) => void;
|
||||
setScripts: (scripts: { [k: string]: string }) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const useBlockScriptStore = create<BlockScriptStore>((set) => {
|
||||
return {
|
||||
scriptId: undefined,
|
||||
scripts: {},
|
||||
// --
|
||||
setScript: (blockId: string, script: string) => {
|
||||
set((state) => ({
|
||||
scripts: {
|
||||
...state.scripts,
|
||||
[blockId]: script,
|
||||
},
|
||||
}));
|
||||
},
|
||||
setScripts: (scripts: { [k: string]: string }) => {
|
||||
set(() => ({
|
||||
scripts,
|
||||
}));
|
||||
},
|
||||
reset: () => {
|
||||
set({
|
||||
scripts: {},
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export { useBlockScriptStore };
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
type DeleteNodeCallback = (id: string) => void;
|
||||
|
||||
const DeleteNodeCallbackContext = createContext<DeleteNodeCallback | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export { DeleteNodeCallbackContext };
|
||||
Reference in New Issue
Block a user