diff --git a/src/frontend/src/CustomNodes/hooks/use-merge-refs.tsx b/src/frontend/src/CustomNodes/hooks/use-merge-refs.tsx
new file mode 100644
index 000000000..383fe5c01
--- /dev/null
+++ b/src/frontend/src/CustomNodes/hooks/use-merge-refs.tsx
@@ -0,0 +1,35 @@
+import { MutableRefObject, useEffect, useRef } from "react";
+
+type Ref
= MutableRefObject | ((instance: T | null) => void);
+
+export function isRefCallback(
+ ref: Ref,
+): ref is (instance: T | null) => void {
+ return typeof ref === "function";
+}
+
+export function isRefObject(ref: Ref): ref is MutableRefObject {
+ return !isRefCallback(ref);
+}
+
+function useMergeRefs(
+ ...refs: (Ref | null | undefined)[]
+): Ref {
+ const targetRef: Ref = useRef(null);
+
+ useEffect(() => {
+ refs.forEach((ref) => {
+ if (!ref) return;
+
+ if (typeof ref === "function") {
+ ref(targetRef.current);
+ } else {
+ ref.current = targetRef.current as T;
+ }
+ });
+ }, [refs]);
+
+ return targetRef;
+}
+
+export default useMergeRefs;
diff --git a/src/frontend/src/components/multiselectComponent/index.tsx b/src/frontend/src/components/multiselectComponent/index.tsx
new file mode 100644
index 000000000..6b6e7c468
--- /dev/null
+++ b/src/frontend/src/components/multiselectComponent/index.tsx
@@ -0,0 +1,326 @@
+"use client";
+
+import { VariantProps, cva } from "class-variance-authority";
+import isEqual from "lodash.isequal";
+import { CheckIcon, ChevronDown, XCircle, XIcon } from "lucide-react";
+import { forwardRef, useEffect, useRef, useState } from "react";
+
+import useMergeRefs, {
+ isRefObject,
+} from "../../CustomNodes/hooks/use-merge-refs";
+import { cn } from "../../utils/utils";
+import { Badge } from "../ui/badge";
+import { Button } from "../ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "../ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverContentWithoutPortal,
+ PopoverTrigger,
+} from "../ui/popover";
+import { Separator } from "../ui/separator";
+
+const MultiselectBadgeWrapper = ({
+ value,
+ variant,
+ className,
+ onDelete,
+}: {
+ value: MultiselectValue;
+ variant: MultiselectProps["variant"];
+ className: MultiselectProps["className"];
+ onDelete: ({ value }: { value: MultiselectValue }) => void;
+}) => {
+ const badgeRef = useRef(null);
+
+ const handleDelete = (
+ event: React.MouseEvent,
+ ) => {
+ event.stopPropagation();
+ onDelete({ value });
+ };
+
+ return (
+
+
+ {value?.label}
+
+
+
+
+
+
+ );
+};
+
+const multiselectVariants = cva("m-1 ", {
+ variants: {
+ variant: {
+ default:
+ "border-foreground/10 text-foreground bg-card hover:bg-card/80 whitespace-normal",
+ secondary:
+ "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80 whitespace-normal",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80 whitespace-normal",
+ inverted: "inverted",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+});
+
+type MultiselectValue = {
+ label: string;
+ value: string;
+};
+
+interface MultiselectProps
+ extends Omit, "value">,
+ VariantProps {
+ options: T[];
+ onValueChange: (value: T[]) => void;
+ placeholder?: string;
+ asChild?: boolean;
+ className?: string;
+ editNode?: boolean;
+ value?: T[];
+}
+
+export const Multiselect = forwardRef<
+ HTMLButtonElement,
+ MultiselectProps
+>(
+ (
+ {
+ options = [],
+ onValueChange,
+ variant,
+ placeholder = "Select options",
+ asChild = false,
+ className,
+ editNode = false,
+ value,
+ ...props
+ },
+ ref,
+ ) => {
+ const [selectedValues, setSelectedValues] = useState(
+ value || [],
+ );
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const combinedRef = useMergeRefs(ref);
+
+ useEffect(() => {
+ if (!!value && value?.length > 0 && !isEqual(selectedValues, value)) {
+ setSelectedValues(value);
+ }
+ }, [value, selectedValues]);
+
+ const handleInputKeyDown = (
+ event: React.KeyboardEvent,
+ ) => {
+ if (event.key === "Enter") {
+ setIsPopoverOpen(true);
+ } else if (event.key === "Backspace" && !event.currentTarget.value) {
+ const newSelectedValues = [...selectedValues];
+ newSelectedValues.pop();
+ setSelectedValues(newSelectedValues);
+ onValueChange(newSelectedValues);
+ }
+ };
+
+ const toggleOption = ({ value }: { value: MultiselectValue }) => {
+ const newSelectedValues = !!selectedValues.find(
+ (v) => v.value === value.value,
+ )
+ ? selectedValues.filter((v) => v.value !== value.value)
+ : [...selectedValues, value];
+ setSelectedValues(newSelectedValues);
+ onValueChange(newSelectedValues);
+ };
+
+ const handleClear = () => {
+ setSelectedValues([]);
+ onValueChange([]);
+ };
+
+ const handleTogglePopover = () => {
+ setIsPopoverOpen((prev) => !prev);
+ };
+
+ const PopoverContentMultiselect = editNode
+ ? PopoverContent
+ : PopoverContentWithoutPortal;
+
+ const popoverContentMultiselectMinWidth = isRefObject(combinedRef)
+ ? `${combinedRef?.current?.clientWidth}px`
+ : "200px";
+
+ return (
+
+
+
+
+ setIsPopoverOpen(false)}
+ >
+
+
+
+ No results found.
+
+ {options?.map((option) => {
+ const isSelected = !!selectedValues.find(
+ (sv) => sv.value === option.value,
+ );
+ return (
+ toggleOption({ value: option })}
+ className="cursor-pointer"
+ >
+
+
+
+ {option.label}
+
+ );
+ })}
+
+
+
+
+ {selectedValues?.length > 0 && (
+ <>
+
+ Clear
+
+
+ >
+ )}
+
+ setIsPopoverOpen(false)}
+ className="flex-1 cursor-pointer justify-center"
+ >
+ Close
+
+
+
+
+
+
+
+ );
+ },
+);
+
+Multiselect.displayName = "Multiselect";
diff --git a/src/frontend/src/components/tableComponent/components/tableNodeCellRender/cellTypeStr.tsx b/src/frontend/src/components/tableComponent/components/tableNodeCellRender/cellTypeStr.tsx
new file mode 100644
index 000000000..7b32ab232
--- /dev/null
+++ b/src/frontend/src/components/tableComponent/components/tableNodeCellRender/cellTypeStr.tsx
@@ -0,0 +1,72 @@
+import Dropdown from "../../../dropdownComponent";
+import InputGlobalComponent from "../../../inputGlobalComponent";
+import InputListComponent from "../../../inputListComponent";
+import { Multiselect } from "../../../multiselectComponent";
+import TextAreaComponent from "../../../textAreaComponent";
+
+export function renderStrType({
+ templateData,
+ templateValue,
+ disabled,
+ handleOnNewValue,
+}) {
+ if (!templateData.options) {
+ return templateData?.list ? (
+ {
+ handleOnNewValue(value, templateData.key);
+ }}
+ />
+ ) : templateData.multiline ? (
+ {
+ handleOnNewValue(value, templateData.key);
+ }}
+ />
+ ) : (
+
+ handleOnNewValue(value, templateData.key, dbValue)
+ }
+ name={templateData.key}
+ data={templateData}
+ />
+ );
+ }
+
+ if (!!templateData.options && !!templateData?.list) {
+ return (
+ handleOnNewValue(value, templateData.key)}
+ />
+ );
+ }
+
+ if (!!templateData.options) {
+ return (
+ handleOnNewValue(value, templateData.key)}
+ value={templateValue ?? "Choose an option"}
+ id={"dropdown-edit-" + templateData.name}
+ />
+ );
+ }
+}
diff --git a/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx b/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx
index 8482a0da4..83c1d1d25 100644
--- a/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx
+++ b/src/frontend/src/components/tableComponent/components/tableNodeCellRender/index.tsx
@@ -22,6 +22,7 @@ import KeypairListComponent from "../../../keypairListComponent";
import PromptAreaComponent from "../../../promptComponent";
import TextAreaComponent from "../../../textAreaComponent";
import ToggleShadComponent from "../../../toggleShadComponent";
+import { renderStrType } from "./cellTypeStr";
export default function TableNodeCellRender({
node: { data },
@@ -75,52 +76,12 @@ export default function TableNodeCellRender({
function getCellType() {
switch (templateData.type) {
case "str":
- if (!templateData.options) {
- return templateData?.list ? (
- {
- handleOnNewValue(value, templateData.key);
- }}
- />
- ) : templateData.multiline ? (
- {
- handleOnNewValue(value, templateData.key);
- }}
- />
- ) : (
-
- handleOnNewValue(value, templateData.key, dbValue)
- }
- name={templateData.key}
- data={templateData}
- />
- );
- } else {
- return (
- handleOnNewValue(value, templateData.key)}
- value={templateValue ?? "Choose an option"}
- id={"dropdown-edit-" + templateData.name}
- />
- );
- }
+ return renderStrType({
+ templateData,
+ templateValue,
+ disabled,
+ handleOnNewValue,
+ });
case "NestedDict":
return (
diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css
index 23ebfe52b..cdf769915 100644
--- a/src/frontend/src/style/applies.css
+++ b/src/frontend/src/style/applies.css
@@ -546,6 +546,13 @@
@apply h-5 w-5 text-primary;
}
+ .multiselect-component-outline {
+ @apply input-edit-node relative pr-8;
+ }
+ .multiselect-component-false-outline {
+ @apply primary-input py-2 pl-3 pr-10 text-left;
+ }
+
.edit-flow-arrangement {
@apply flex justify-between;
}