Skip to content

Commit

Permalink
feat: add agent and workflow dropdowns to workflow steps (#645)
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Hopper-Lowe <[email protected]>
  • Loading branch information
ryanhopperlowe authored Nov 21, 2024
1 parent 8af3b96 commit 794e6e2
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 20 deletions.
58 changes: 58 additions & 0 deletions ui/admin/app/components/agent/shared/AgentSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import useSWR from "swr";

import { Agent } from "~/lib/model/agents";
import { AgentService } from "~/lib/service/api/agentService";

import { SelectModule } from "~/components/composed/SelectModule";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip";

type AgentSelectModuleProps = {
onChange: (agents: string[]) => void;
selection: string[];
};

export function AgentSelectModule(props: AgentSelectModuleProps) {
const { data: agents } = useSWR(
AgentService.getAgents.key(),
AgentService.getAgents
);

return (
<SelectModule
selection={props.selection}
onChange={props.onChange}
renderDropdownItem={(agent) => <AgentText agent={agent} />}
renderListItem={(agent) => <AgentText agent={agent} />}
getItemKey={(agent) => agent.id}
buttonText="Add Agent"
items={agents}
/>
);
}

function AgentText({ agent }: { agent: Agent }) {
const content = (
<div className="flex items-center gap-2 overflow-hidden">
<span className="min-w-fit">{agent.name}</span>
{agent.description && (
<>
<span>-</span>
<span className="text-muted-foreground truncate">
{agent.description}
</span>
</>
)}
</div>
);

return (
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
</Tooltip>
);
}
185 changes: 185 additions & 0 deletions ui/admin/app/components/composed/SelectModule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { PlusIcon, TrashIcon } from "lucide-react";
import { useMemo } from "react";

import { Button } from "~/components/ui/button";
import {
Command,
CommandEmpty,
CommandInput,
CommandItem,
CommandList,
} from "~/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";

interface SelectModuleProps<T> {
items?: T[];
selection: string[];
onChange: (selection: string[]) => void;
renderDropdownItem: (item: T) => React.ReactNode;
renderListItem: (item: T) => React.ReactNode;
buttonText?: string;
searchPlaceholder?: string;
emptyMessage?: string;
getItemKey: (item: T) => string;
}

export function SelectModule<T>({
items = [],
selection,
onChange,
renderDropdownItem,
renderListItem,
buttonText,
searchPlaceholder,
emptyMessage,
getItemKey,
}: SelectModuleProps<T>) {
return (
<div className="flex flex-col gap-2">
<SelectList
selected={selection}
items={items}
onRemove={(id) => onChange(selection.filter((s) => s !== id))}
renderItem={renderListItem}
getItemKey={getItemKey}
/>

<SelectPopover
className="self-end"
items={items}
onSelect={(item) => onChange([...selection, getItemKey(item)])}
filter={(item) => !selection.includes(getItemKey(item))}
renderItem={renderDropdownItem}
buttonText={buttonText}
searchPlaceholder={searchPlaceholder}
emptyMessage={emptyMessage}
getItemKey={getItemKey}
/>
</div>
);
}

interface SelectProps<T> {
items?: T[];
onSelect: (item: T) => void;
filter?: (item: T, index: number, array: T[]) => boolean;
renderItem: (item: T) => React.ReactNode;
searchPlaceholder?: string;
emptyMessage?: string;
getItemKey: (item: T) => string;
}

export function Select<T>({
items = [],
onSelect,
filter,
renderItem,
searchPlaceholder = "Search...",
emptyMessage = "No items to select",
getItemKey,
}: SelectProps<T>) {
const filteredItems = filter ? items.filter(filter) : items;

return (
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
{filteredItems?.length ? (
filteredItems?.map((item) => (
<CommandItem
key={getItemKey(item)}
value={getItemKey(item)}
onSelect={() => onSelect(item)}
>
{renderItem(item)}
</CommandItem>
))
) : (
<CommandEmpty>{emptyMessage}</CommandEmpty>
)}
</CommandList>
</Command>
);
}

interface SelectPopoverProps<T> extends SelectProps<T> {
className?: string;
buttonText?: string;
}

export function SelectPopover<T>({
className,
buttonText = "Select Item",
...props
}: SelectPopoverProps<T>) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="secondary"
startContent={<PlusIcon />}
className={className}
>
{buttonText}
</Button>
</PopoverTrigger>

<PopoverContent className="p-0" align="end">
<Select {...props} />
</PopoverContent>
</Popover>
);
}

interface SelectListProps<T> {
selected: string[];
items?: T[];
onRemove: (id: string) => void;
renderItem: (item: T) => React.ReactNode;
fallbackRender?: (id: string) => React.ReactNode;
getItemKey: (item: T) => string;
}

export function SelectList<T>({
selected,
items = [],
onRemove,
renderItem,
fallbackRender = (id) => id,
getItemKey,
}: SelectListProps<T>) {
const itemMap = useMemo(() => {
return items.reduce(
(acc, item) => {
acc[getItemKey(item)] = item;
return acc;
},
{} as Record<string, T>
);
}, [items, getItemKey]);

return (
<div className="flex flex-col gap-2 divide-y">
{selected.map((id) => (
<div
key={id}
className="flex items-center justify-between gap-2 pt-2"
>
{itemMap[id] ? renderItem(itemMap[id]) : fallbackRender(id)}

<Button
variant="ghost"
size="icon"
onClick={() => onRemove(id)}
>
<TrashIcon />
</Button>
</div>
))}
</div>
);
}
2 changes: 1 addition & 1 deletion ui/admin/app/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const buttonVariants = cva(
badge: "text-xs py-0.5 px-2",
sm: "h-8 px-3 text-xs",
lg: "h-10 px-8",
icon: "h-9 w-9 [&_svg]:size-[1.375rem]",
icon: "h-9 w-9 min-w-9 [&_svg]:size-[1.375rem]",
},
shape: {
default: "rounded-md",
Expand Down
60 changes: 60 additions & 0 deletions ui/admin/app/components/workflow/WorkflowSelectModule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import useSWR from "swr";

import { Workflow } from "~/lib/model/workflows";
import { WorkflowService } from "~/lib/service/api/workflowService";

import { SelectModule } from "~/components/composed/SelectModule";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip";

type WorkflowSelectModuleProps = {
onChange: (workflows: string[]) => void;
selection: string[];
};

export function WorkflowSelectModule(props: WorkflowSelectModuleProps) {
const { data: workflows } = useSWR(
WorkflowService.getWorkflows.key(),
WorkflowService.getWorkflows
);

return (
<SelectModule
selection={props.selection}
onChange={props.onChange}
getItemKey={(workflow) => workflow.id}
renderDropdownItem={(workflow) => (
<WorkflowText workflow={workflow} />
)}
renderListItem={(workflow) => <WorkflowText workflow={workflow} />}
buttonText="Add Workflow"
items={workflows}
/>
);
}

function WorkflowText({ workflow }: { workflow: Workflow }) {
const content = (
<div className="flex items-center gap-2 overflow-hidden">
<span className="min-w-fit">{workflow.name}</span>
{workflow.description && (
<>
<span>-</span>
<span className="text-muted-foreground truncate">
{workflow.description}
</span>
</>
)}
</div>
);

return (
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
</Tooltip>
);
}
29 changes: 10 additions & 19 deletions ui/admin/app/components/workflow/steps/Step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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,
Expand All @@ -25,7 +26,7 @@ import { ButtonDiv } from "~/components/ui/clickable-div";
import { Input } from "~/components/ui/input";
import { Switch } from "~/components/ui/switch";
import { AutosizeTextarea } from "~/components/ui/textarea";
import { StringArrayForm } from "~/components/workflow/StringArrayForm";
import { WorkflowSelectModule } from "~/components/workflow/WorkflowSelectModule";

export function StepComponent({
step,
Expand Down Expand Up @@ -123,16 +124,11 @@ export function StepComponent({
</AccordionTrigger>

<AccordionContent className="p-1 pb-6">
<StringArrayForm
initialItems={step.workflows || []}
onChange={(values) =>
onUpdate({
...step,
workflows: values.items,
})
<WorkflowSelectModule
onChange={(workflows) =>
onUpdate({ ...step, workflows })
}
itemName="Workflow"
placeholder="Add a workflow"
selection={step.workflows || []}
/>
</AccordionContent>
</AccordionItem>
Expand All @@ -146,16 +142,11 @@ export function StepComponent({
</AccordionTrigger>

<AccordionContent className="p-1 pb-6">
<StringArrayForm
initialItems={step.agents || []}
onChange={(values) =>
onUpdate({
...step,
agents: values.items,
})
<AgentSelectModule
onChange={(agents) =>
onUpdate({ ...step, agents })
}
itemName="Agent"
placeholder="Add an agent"
selection={step.agents || []}
/>
</AccordionContent>
</AccordionItem>
Expand Down

0 comments on commit 794e6e2

Please sign in to comment.