From d55381081ac0632d23932ec4d8c5339c1d448a74 Mon Sep 17 00:00:00 2001
From: Ryan Hopper-Lowe <46546486+ryanhopperlowe@users.noreply.github.com>
Date: Tue, 31 Dec 2024 17:09:01 -0600
Subject: [PATCH] enhance: update workflow step UX (#1087)
- adding a step auto-adds a generic step. No longer need to specify step type when adding
- step types can be updated via dropdown on the step list item
---
ui/admin/app/components/ui/textarea.tsx | 6 +-
.../app/components/workflow/steps/AddStep.tsx | 135 +-----------
ui/admin/app/components/workflow/steps/If.tsx | 154 -------------
.../components/workflow/steps/IfContent.tsx | 79 +++++++
.../app/components/workflow/steps/Step.tsx | 208 ------------------
.../components/workflow/steps/StepBase.tsx | 116 ++++++++++
.../components/workflow/steps/StepContent.tsx | 127 +++++++++++
.../workflow/steps/StepRenderer.tsx | 55 +++--
.../workflow/steps/StepTypeSelect.tsx | 65 ++++++
.../app/components/workflow/steps/While.tsx | 153 -------------
.../workflow/steps/WhileContent.tsx | 83 +++++++
ui/admin/app/lib/model/workflows.ts | 40 +++-
12 files changed, 547 insertions(+), 674 deletions(-)
delete mode 100644 ui/admin/app/components/workflow/steps/If.tsx
create mode 100644 ui/admin/app/components/workflow/steps/IfContent.tsx
delete mode 100644 ui/admin/app/components/workflow/steps/Step.tsx
create mode 100644 ui/admin/app/components/workflow/steps/StepBase.tsx
create mode 100644 ui/admin/app/components/workflow/steps/StepContent.tsx
create mode 100644 ui/admin/app/components/workflow/steps/StepTypeSelect.tsx
delete mode 100644 ui/admin/app/components/workflow/steps/While.tsx
create mode 100644 ui/admin/app/components/workflow/steps/WhileContent.tsx
diff --git a/ui/admin/app/components/ui/textarea.tsx b/ui/admin/app/components/ui/textarea.tsx
index b99fb0459..79d76c0fc 100644
--- a/ui/admin/app/components/ui/textarea.tsx
+++ b/ui/admin/app/components/ui/textarea.tsx
@@ -4,12 +4,14 @@ import { forwardRef, useImperativeHandle } from "react";
import { cn } from "~/lib/utils";
+// note: use outline instead of ring to avoid overriding the ring from the outline variant
const textareaVariants = cva(
- "flex w-full rounded-md bg-transparent text-sm placeholder:text-muted-foreground has-[:focus-visible]:ring-1 has-[:focus-visible]:ring-ring group group-disabled:cursor-not-allowed group-disabled:bg-opacity-50",
+ "flex w-full rounded-md bg-transparent text-sm placeholder:text-muted-foreground has-[:focus-visible]:outline has-[:focus-visible]:outline-1 has-[:focus-visible]:outline-ring group group-disabled:cursor-not-allowed group-disabled:bg-opacity-50",
{
variants: {
variant: {
- outlined: "border border-input shadow-sm",
+ // note: use inset ring instead of border so that the wrapper doesn't add any extra height or width
+ outlined: "ring-1 ring-inset ring-input",
flat: "border-none shadow-none bg-muted",
},
},
diff --git a/ui/admin/app/components/workflow/steps/AddStep.tsx b/ui/admin/app/components/workflow/steps/AddStep.tsx
index 1eea3f0f2..4a4bd8325 100644
--- a/ui/admin/app/components/workflow/steps/AddStep.tsx
+++ b/ui/admin/app/components/workflow/steps/AddStep.tsx
@@ -1,30 +1,8 @@
-import { ArrowRight, GitFork, Plus, RotateCw } from "lucide-react";
-import { useMemo, useState } from "react";
-import useSWR from "swr";
+import { PlusIcon } from "lucide-react";
-import {
- ToolReference,
- toolReferenceToTemplate,
-} from "~/lib/model/toolReferences";
import { Step, getDefaultStep } from "~/lib/model/workflows";
-import { ToolReferenceService } from "~/lib/service/api/toolreferenceService";
import { Button } from "~/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "~/components/ui/dialog";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "~/components/ui/popover";
-import { ScrollArea } from "~/components/ui/scroll-area";
-import { StepTemplateCard } from "~/components/workflow/steps/StepTemplateCard";
-
-type StepType = "regular" | "if" | "while" | "template";
interface AddStepButtonProps {
onAddStep: (newStep: Step) => void;
@@ -32,109 +10,14 @@ interface AddStepButtonProps {
}
export function AddStepButton({ onAddStep, className }: AddStepButtonProps) {
- const [open, setOpen] = useState(false);
- const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false);
-
- const getStepTemplates = useSWR(
- ToolReferenceService.getToolReferences.key("stepTemplate"),
- () => ToolReferenceService.getToolReferences("stepTemplate")
- );
-
- const stepTemplates = useMemo(() => {
- if (!getStepTemplates.data) return [];
- return getStepTemplates.data;
- }, [getStepTemplates.data]);
-
- const createNewStep = (type: StepType) => {
- if (type === "template") {
- setIsTemplateModalOpen(true);
- } else {
- const newStep = getDefaultStep();
-
- if (type === "if") {
- newStep.if = { condition: "", steps: [], else: [] };
- } else if (type === "while") {
- newStep.while = { condition: "", maxLoops: 0, steps: [] };
- }
-
- onAddStep(newStep);
- setOpen(false);
- }
- };
-
- const handleStepTemplateSelection = (stepTemplate: ToolReference) => {
- const newStep: Step = {
- id: Date.now().toString(),
- name: stepTemplate.name,
- description: "",
- template: toolReferenceToTemplate(stepTemplate),
- cache: false,
- temperature: 0,
- tools: [],
- agents: [],
- workflows: [],
- };
- onAddStep(newStep);
- setIsTemplateModalOpen(false);
- };
-
return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
+ }
+ onClick={() => onAddStep(getDefaultStep())}
+ >
+ Add Step
+
);
}
diff --git a/ui/admin/app/components/workflow/steps/If.tsx b/ui/admin/app/components/workflow/steps/If.tsx
deleted file mode 100644
index 8079985bd..000000000
--- a/ui/admin/app/components/workflow/steps/If.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import { ChevronDown, ChevronRight, GitFork, Trash } from "lucide-react";
-import { useState } from "react";
-
-import { If, Step } from "~/lib/model/workflows";
-import { cn } from "~/lib/utils";
-
-import { Button } from "~/components/ui/button";
-import { ClickableDiv } from "~/components/ui/clickable-div";
-import { AutosizeTextarea } from "~/components/ui/textarea";
-import { AddStepButton } from "~/components/workflow/steps/AddStep";
-
-export function IfComponent({
- ifCondition,
- onUpdate,
- onDelete,
- renderStep,
- className,
-}: {
- ifCondition: If;
- onUpdate: (updatedIf: If) => void;
- onDelete: () => void;
- renderStep: (
- step: Step,
- onUpdate: (updatedStep: Step) => void,
- onDelete: () => void
- ) => React.ReactNode;
- className?: string;
-}) {
- const [isExpanded, setIsExpanded] = useState(false);
-
- const addStep = (branch: "steps" | "else") => (newStep: Step) => {
- onUpdate({
- ...ifCondition,
- [branch]: [...(ifCondition[branch] || []), newStep],
- });
- };
-
- const updateNestedStep = (
- branch: "steps" | "else",
- index: number,
- updatedStep: Step
- ) => {
- const newBranch = [...(ifCondition[branch] || [])];
- newBranch[index] = updatedStep;
- onUpdate({ ...ifCondition, [branch]: newBranch });
- };
-
- const deleteNestedStep = (branch: "steps" | "else", index: number) => {
- const newBranch = (ifCondition[branch] || []).filter(
- (_, i) => i !== index
- );
- onUpdate({ ...ifCondition, [branch]: newBranch });
- };
-
- return (
-
-
setIsExpanded((prev) => !prev)}
- >
-
-
-
-
-
- If
-
-
-
-
- onUpdate({ ...ifCondition, condition: e.target.value })
- }
- onClick={(e) => e.stopPropagation()}
- minHeight={0}
- maxHeight={100}
- placeholder="Condition"
- className="flex-grow bg-background"
- />
-
-
-
-
- {isExpanded && (
-
-
-
Then:
- {ifCondition.steps?.map((step, index) => (
-
- {renderStep(
- step,
- (updatedStep) =>
- updateNestedStep(
- "steps",
- index,
- updatedStep
- ),
- () => deleteNestedStep("steps", index)
- )}
-
- ))}
-
-
-
-
Else:
- {ifCondition.else?.map((step, index) => (
-
- {renderStep(
- step,
- (updatedStep) =>
- updateNestedStep(
- "else",
- index,
- updatedStep
- ),
- () => deleteNestedStep("else", index)
- )}
-
- ))}
-
-
-
- )}
-
- );
-}
diff --git a/ui/admin/app/components/workflow/steps/IfContent.tsx b/ui/admin/app/components/workflow/steps/IfContent.tsx
new file mode 100644
index 000000000..fe50a2cc0
--- /dev/null
+++ b/ui/admin/app/components/workflow/steps/IfContent.tsx
@@ -0,0 +1,79 @@
+import { If, Step } from "~/lib/model/workflows";
+
+import { AddStepButton } from "~/components/workflow/steps/AddStep";
+
+export function IfContent({
+ ifCondition,
+ onUpdate,
+ renderStep,
+}: {
+ ifCondition: If;
+ onUpdate: (updatedIf: If) => void;
+ renderStep: (
+ step: Step,
+ onUpdate: (updatedStep: Step) => void,
+ onDelete: () => void
+ ) => React.ReactNode;
+ className?: string;
+}) {
+ const addStep = (branch: "steps" | "else") => (newStep: Step) => {
+ onUpdate({
+ ...ifCondition,
+ [branch]: [...(ifCondition[branch] || []), newStep],
+ });
+ };
+
+ const updateNestedStep = (
+ branch: "steps" | "else",
+ index: number,
+ updatedStep: Step
+ ) => {
+ const newBranch = [...(ifCondition[branch] || [])];
+ newBranch[index] = updatedStep;
+ onUpdate({ ...ifCondition, [branch]: newBranch });
+ };
+
+ const deleteNestedStep = (branch: "steps" | "else", index: number) => {
+ const newBranch = (ifCondition[branch] || []).filter(
+ (_, i) => i !== index
+ );
+ onUpdate({ ...ifCondition, [branch]: newBranch });
+ };
+
+ return (
+
+
+
Then:
+ {ifCondition.steps?.map((step, index) => (
+
+ {renderStep(
+ step,
+ (updatedStep) =>
+ updateNestedStep("steps", index, updatedStep),
+ () => deleteNestedStep("steps", index)
+ )}
+
+ ))}
+
+
+
+
Else:
+ {ifCondition.else?.map((step, index) => (
+
+ {renderStep(
+ step,
+ (updatedStep) =>
+ updateNestedStep("else", index, updatedStep),
+ () => deleteNestedStep("else", index)
+ )}
+
+ ))}
+
+
+
+ );
+}
diff --git a/ui/admin/app/components/workflow/steps/Step.tsx b/ui/admin/app/components/workflow/steps/Step.tsx
deleted file mode 100644
index 80d487aa8..000000000
--- a/ui/admin/app/components/workflow/steps/Step.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-import {
- ArrowRight,
- ChevronDown,
- ChevronRight,
- CogIcon,
- Puzzle,
- Trash,
- User,
- Wrench,
-} from "lucide-react";
-import { useState } from "react";
-
-import { Step } from "~/lib/model/workflows";
-import { cn } from "~/lib/utils";
-
-import { AgentSelectModule } from "~/components/agent/shared/AgentSelect";
-import { BasicToolForm } from "~/components/tools/BasicToolForm";
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "~/components/ui/accordion";
-import { Button } from "~/components/ui/button";
-import { ClickableDiv } from "~/components/ui/clickable-div";
-import { Input } from "~/components/ui/input";
-import { Switch } from "~/components/ui/switch";
-import { AutosizeTextarea } from "~/components/ui/textarea";
-import { WorkflowSelectModule } from "~/components/workflow/WorkflowSelectModule";
-
-export function StepComponent({
- step,
- onUpdate,
- onDelete,
- className,
-}: {
- step: Step;
- onUpdate: (updatedStep: Step) => void;
- onDelete: () => void;
- className?: string;
-}) {
- const [isExpanded, setIsExpanded] = useState(false);
-
- return (
-
-
setIsExpanded((prev) => !prev)}
- >
-
-
-
-
-
-
-
- onUpdate({ ...step, step: e.target.value })
- }
- placeholder="Step"
- maxHeight={100}
- minHeight={0}
- className="flex-grow bg-background-secondary"
- onClick={(e) => e.stopPropagation()}
- />
-
-
-
-
- {isExpanded && (
-
-
-
-
-
-
- Tools
-
-
-
-
-
- onUpdate({ ...step, tools })
- }
- />
-
-
-
-
-
-
-
- Workflows
-
-
-
-
-
- onUpdate({ ...step, workflows })
- }
- selection={step.workflows || []}
- />
-
-
-
-
-
-
-
- Agents
-
-
-
-
-
- onUpdate({ ...step, agents })
- }
- selection={step.agents || []}
- />
-
-
-
-
-
-
-
- Advanced
-
-
-
-
-
-
-
-
- onUpdate({
- ...step,
- temperature: parseFloat(
- e.target.value
- ),
- })
- }
- placeholder="Temperature"
- className="bg-background"
- />
-
-
-
-
- onUpdate({
- ...step,
- cache: checked,
- })
- }
- />
-
- Cache
-
-
-
-
-
- )}
-
- );
-}
diff --git a/ui/admin/app/components/workflow/steps/StepBase.tsx b/ui/admin/app/components/workflow/steps/StepBase.tsx
new file mode 100644
index 000000000..18a2d0b24
--- /dev/null
+++ b/ui/admin/app/components/workflow/steps/StepBase.tsx
@@ -0,0 +1,116 @@
+import { ChevronDown, ChevronRight, Trash } from "lucide-react";
+import { useState } from "react";
+
+import { Step, StepType, getDefaultStep } from "~/lib/model/workflows";
+import { cn } from "~/lib/utils";
+
+import { Button } from "~/components/ui/button";
+import { ClickableDiv } from "~/components/ui/clickable-div";
+import { AutosizeTextarea } from "~/components/ui/textarea";
+import { StepTypeSelect } from "~/components/workflow/steps/StepTypeSelect";
+
+export function StepBase({
+ className,
+ children,
+ step,
+ type,
+ onUpdate,
+ onDelete,
+}: {
+ className?: string;
+ children: React.ReactNode;
+ step: Step;
+ type: StepType;
+ onUpdate: (step: Step) => void;
+ onDelete: () => void;
+}) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ const fieldConfig = getTextFieldConfig();
+
+ return (
+
+
setIsExpanded((prev) => !prev)}
+ >
+
+
+
+ {
+ if (newType !== type) {
+ onUpdate(getDefaultStep(newType));
+ }
+ }}
+ />
+
+
+ fieldConfig.onChange(e.target.value)}
+ placeholder={fieldConfig.placeholder}
+ maxHeight={100}
+ minHeight={0}
+ className="flex-grow bg-background-secondary"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+
+
+ {isExpanded && children}
+
+ );
+
+ function getTextFieldConfig() {
+ if (type === "if" && step.if) {
+ const copy = step.if;
+
+ return {
+ placeholder: "Condition",
+ value: step.if.condition,
+ onChange: (value: string) =>
+ onUpdate({ ...step, if: { ...copy, condition: value } }),
+ };
+ } else if (type === "while" && step.while) {
+ const copy = step.while;
+
+ return {
+ placeholder: "Condition",
+ value: step.while.condition,
+ onChange: (value: string) =>
+ onUpdate({ ...step, while: { ...copy, condition: value } }),
+ };
+ }
+
+ return {
+ placeholder: "Step",
+ value: step.step,
+ onChange: (value: string) => onUpdate({ ...step, step: value }),
+ };
+ }
+}
diff --git a/ui/admin/app/components/workflow/steps/StepContent.tsx b/ui/admin/app/components/workflow/steps/StepContent.tsx
new file mode 100644
index 000000000..89a148181
--- /dev/null
+++ b/ui/admin/app/components/workflow/steps/StepContent.tsx
@@ -0,0 +1,127 @@
+import { CogIcon, Puzzle, User, Wrench } from "lucide-react";
+
+import { Step } from "~/lib/model/workflows";
+
+import { AgentSelectModule } from "~/components/agent/shared/AgentSelect";
+import { BasicToolForm } from "~/components/tools/BasicToolForm";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "~/components/ui/accordion";
+import { Input } from "~/components/ui/input";
+import { Switch } from "~/components/ui/switch";
+import { WorkflowSelectModule } from "~/components/workflow/WorkflowSelectModule";
+
+export function StepContent({
+ step,
+ onUpdate,
+}: {
+ step: Step;
+ onUpdate: (updatedStep: Step) => void;
+}) {
+ return (
+
+
+
+
+
+
+ Tools
+
+
+
+
+ onUpdate({ ...step, tools })}
+ />
+
+
+
+
+
+
+
+ Workflows
+
+
+
+
+
+ onUpdate({ ...step, workflows })
+ }
+ selection={step.workflows || []}
+ />
+
+
+
+
+
+
+
+ Agents
+
+
+
+
+ onUpdate({ ...step, agents })}
+ selection={step.agents || []}
+ />
+
+
+
+
+
+
+
+ Advanced
+
+
+
+
+
+
+
+
+ onUpdate({
+ ...step,
+ temperature: parseFloat(e.target.value),
+ })
+ }
+ placeholder="Temperature"
+ className="bg-background"
+ />
+
+
+
+
+ onUpdate({
+ ...step,
+ cache: checked,
+ })
+ }
+ />
+
+ Cache
+
+
+
+
+
+ );
+}
diff --git a/ui/admin/app/components/workflow/steps/StepRenderer.tsx b/ui/admin/app/components/workflow/steps/StepRenderer.tsx
index ae8a8f87c..975be532b 100644
--- a/ui/admin/app/components/workflow/steps/StepRenderer.tsx
+++ b/ui/admin/app/components/workflow/steps/StepRenderer.tsx
@@ -1,9 +1,10 @@
import { Step } from "~/lib/model/workflows";
-import { IfComponent } from "~/components/workflow/steps/If";
-import { StepComponent } from "~/components/workflow/steps/Step";
+import { IfContent } from "~/components/workflow/steps/IfContent";
+import { StepBase } from "~/components/workflow/steps/StepBase";
+import { StepContent } from "~/components/workflow/steps/StepContent";
import { TemplateComponent } from "~/components/workflow/steps/Template";
-import { WhileComponent } from "~/components/workflow/steps/While";
+import { WhileContent } from "~/components/workflow/steps/WhileContent";
export function renderStep(
step: Step,
@@ -21,37 +22,51 @@ export function renderStep(
);
} else if (step.if) {
return (
- onUpdate({ ...step, if: updatedIf })}
+ step={step}
+ type="if"
+ onUpdate={onUpdate}
onDelete={onDelete}
- renderStep={renderStep}
- className="mb-4"
- />
+ >
+
+ onUpdate({ ...step, if: updatedIf })
+ }
+ renderStep={renderStep}
+ />
+
);
} else if (step.while) {
return (
-
- onUpdate({ ...step, while: updatedWhile })
- }
+ step={step}
+ type="while"
+ onUpdate={onUpdate}
onDelete={onDelete}
- renderStep={renderStep}
- className="mb-4"
- />
+ >
+
+ onUpdate({ ...step, while: updatedWhile })
+ }
+ renderStep={renderStep}
+ />
+
);
} else {
return (
-
+ >
+
+
);
}
}
diff --git a/ui/admin/app/components/workflow/steps/StepTypeSelect.tsx b/ui/admin/app/components/workflow/steps/StepTypeSelect.tsx
new file mode 100644
index 000000000..433b92c4c
--- /dev/null
+++ b/ui/admin/app/components/workflow/steps/StepTypeSelect.tsx
@@ -0,0 +1,65 @@
+import { ArrowRight, RotateCwIcon, SplitIcon } from "lucide-react";
+
+import { StepType } from "~/lib/model/workflows";
+
+import { ClickableDiv } from "~/components/ui/clickable-div";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "~/components/ui/select";
+
+type StepSelectProps = {
+ value: StepType;
+ onChange: (type: StepType) => void;
+};
+
+export function StepTypeSelect({ value, onChange }: StepSelectProps) {
+ const options = Object.values(StepType).map((type) => ({
+ id: type,
+ name: StepLabelMap[type],
+ }));
+
+ return (
+ e.stopPropagation()}>
+
+
+ );
+}
+
+const StepLabelMap: Record = {
+ [StepType.Command]: "Step",
+ [StepType.If]: "If",
+ [StepType.While]: "While",
+ // [StepType.Template]: "Template",
+};
+
+const IconMap: Record = {
+ [StepType.Command]: ArrowRight,
+ [StepType.If]: SplitIcon,
+ [StepType.While]: RotateCwIcon,
+ // [StepType.Template]: PuzzleIcon,
+};
+
+function StepTypeIcon({ type }: { type: StepType }) {
+ const Icon = IconMap[type] ?? ArrowRight;
+
+ return ;
+}
diff --git a/ui/admin/app/components/workflow/steps/While.tsx b/ui/admin/app/components/workflow/steps/While.tsx
deleted file mode 100644
index 090fa2089..000000000
--- a/ui/admin/app/components/workflow/steps/While.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import { ChevronDown, ChevronRight, RotateCw, Trash } from "lucide-react";
-import { useState } from "react";
-
-import { Step, While } from "~/lib/model/workflows";
-import { cn } from "~/lib/utils";
-
-import { Button } from "~/components/ui/button";
-import { ClickableDiv } from "~/components/ui/clickable-div";
-import { Input } from "~/components/ui/input";
-import { AutosizeTextarea } from "~/components/ui/textarea";
-import { AddStepButton } from "~/components/workflow/steps/AddStep";
-
-export function WhileComponent({
- whileCondition,
- onUpdate,
- onDelete,
- renderStep,
- className,
-}: {
- whileCondition: While;
- onUpdate: (updatedWhile: While) => void;
- onDelete: () => void;
- renderStep: (
- step: Step,
- onUpdate: (updatedStep: Step) => void,
- onDelete: () => void
- ) => React.ReactNode;
- className?: string;
-}) {
- const [isExpanded, setIsExpanded] = useState(false);
-
- const addStep = (newStep: Step) => {
- onUpdate({
- ...whileCondition,
- steps: [...(whileCondition.steps || []), newStep],
- });
- };
-
- const updateNestedStep = (index: number, updatedStep: Step) => {
- const newSteps = [...(whileCondition.steps || [])];
- newSteps[index] = updatedStep;
- onUpdate({ ...whileCondition, steps: newSteps });
- };
-
- const deleteNestedStep = (index: number) => {
- const newSteps = (whileCondition.steps || []).filter(
- (_, i) => i !== index
- );
- onUpdate({ ...whileCondition, steps: newSteps });
- };
-
- return (
-
-
setIsExpanded((prev) => !prev)}
- >
-
-
-
-
-
- While
-
-
-
-
- onUpdate({
- ...whileCondition,
- condition: e.target.value,
- })
- }
- minHeight={0}
- maxHeight={100}
- placeholder="Condition"
- className="flex-grow bg-background"
- onClick={(e) => e.stopPropagation()}
- />
-
-
-
- {isExpanded && (
-
-
-
-
- onUpdate({
- ...whileCondition,
- maxLoops: parseInt(e.target.value),
- })
- }
- placeholder="Max Loops"
- className="bg-background"
- />
-
-
-
-
Steps:
- {whileCondition.steps?.map((step, index) => (
-
- {renderStep(
- step,
- (updatedStep) =>
- updateNestedStep(index, updatedStep),
- () => deleteNestedStep(index)
- )}
-
- ))}
-
-
-
-
- )}
-
- );
-}
diff --git a/ui/admin/app/components/workflow/steps/WhileContent.tsx b/ui/admin/app/components/workflow/steps/WhileContent.tsx
new file mode 100644
index 000000000..aa3086929
--- /dev/null
+++ b/ui/admin/app/components/workflow/steps/WhileContent.tsx
@@ -0,0 +1,83 @@
+import { Step, While } from "~/lib/model/workflows";
+
+import { Input } from "~/components/ui/input";
+import { AddStepButton } from "~/components/workflow/steps/AddStep";
+
+export function WhileContent({
+ whileCondition,
+ onUpdate,
+ renderStep,
+}: {
+ whileCondition: While;
+ onUpdate: (updatedWhile: While) => void;
+ renderStep: (
+ step: Step,
+ onUpdate: (updatedStep: Step) => void,
+ onDelete: () => void
+ ) => React.ReactNode;
+ className?: string;
+}) {
+ const addStep = (newStep: Step) => {
+ onUpdate({
+ ...whileCondition,
+ steps: [...(whileCondition.steps || []), newStep],
+ });
+ };
+
+ const updateNestedStep = (index: number, updatedStep: Step) => {
+ const newSteps = [...(whileCondition.steps || [])];
+ newSteps[index] = updatedStep;
+ onUpdate({ ...whileCondition, steps: newSteps });
+ };
+
+ const deleteNestedStep = (index: number) => {
+ const newSteps = (whileCondition.steps || []).filter(
+ (_, i) => i !== index
+ );
+ onUpdate({ ...whileCondition, steps: newSteps });
+ };
+
+ return (
+
+
+
+
+ onUpdate({
+ ...whileCondition,
+ maxLoops: parseInt(e.target.value),
+ })
+ }
+ placeholder="Max Loops"
+ className="bg-background"
+ />
+
+
+
+
Steps:
+ {whileCondition.steps?.map((step, index) => (
+
+ {renderStep(
+ step,
+ (updatedStep) =>
+ updateNestedStep(index, updatedStep),
+ () => deleteNestedStep(index)
+ )}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/ui/admin/app/lib/model/workflows.ts b/ui/admin/app/lib/model/workflows.ts
index 19d8f2d75..79e8f043d 100644
--- a/ui/admin/app/lib/model/workflows.ts
+++ b/ui/admin/app/lib/model/workflows.ts
@@ -6,6 +6,14 @@ export type WorkflowBase = AgentBase & {
output: string;
};
+export const StepType = {
+ Command: "command",
+ If: "if",
+ While: "while",
+ // Template: "template",
+} as const;
+export type StepType = (typeof StepType)[keyof typeof StepType];
+
export type Step = {
id: string;
name: string;
@@ -50,14 +58,24 @@ export type Workflow = EntityMeta &
export type CreateWorkflow = Partial & Pick;
export type UpdateWorkflow = WorkflowBase;
-export const getDefaultStep = (): Step => ({
- id: crypto.randomUUID(),
- name: "",
- description: "",
- step: "",
- cache: false,
- temperature: 0,
- tools: [],
- agents: [],
- workflows: [],
-});
+export const getDefaultStep = (type: StepType = StepType.Command): Step => {
+ const newStep: Step = {
+ id: crypto.randomUUID(),
+ name: "",
+ description: "",
+ step: "",
+ cache: false,
+ temperature: 0,
+ tools: [],
+ agents: [],
+ workflows: [],
+ };
+
+ if (type === StepType.If) {
+ newStep.if = { condition: "", steps: [], else: [] };
+ } else if (type === StepType.While) {
+ newStep.while = { condition: "", maxLoops: 0, steps: [] };
+ }
+
+ return newStep;
+};