import * as React from "react"; import { Check, ChevronDown } from "lucide-react"; import { cn } from "@/src/utils/tailwind"; import { Badge } from "@/src/components/ui/badge"; import { Button } from "@/src/components/ui/button"; import { InputCommand, InputCommandEmpty, InputCommandGroup, InputCommandInput, InputCommandItem, InputCommandList, InputCommandSeparator, } from "@/src/components/ui/input-command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/src/components/ui/popover"; import { Separator } from "@/src/components/ui/separator"; import { type FilterOption } from "@langfuse/shared"; import { Input } from "@/src/components/ui/input"; import { useRef, useState, useMemo, useCallback } from "react"; import { PropertyHoverCard } from "@/src/features/widgets/components/WidgetPropertySelectItem"; const getFreeTextInput = ( isCustomSelectEnabled: boolean, values: string[], optionValues: Set, ): string | undefined => isCustomSelectEnabled ? Array.from(values.values()).find((value) => !optionValues.has(value)) : undefined; export function MultiSelect({ title, label, values, onValueChange, options, className, disabled, isCustomSelectEnabled = false, labelTruncateCutOff = 2, }: { title?: string; label?: string; values: string[]; onValueChange: (values: string[]) => void; options: FilterOption[] | readonly FilterOption[]; className?: string; disabled?: boolean; isCustomSelectEnabled?: boolean; labelTruncateCutOff?: number; }) { const selectedValues = useMemo(() => new Set(values), [values]); const optionValues = new Set(options.map((option) => option.value)); const freeTextInput = getFreeTextInput( isCustomSelectEnabled, values, optionValues, ); const [freeText, setFreeText] = useState(freeTextInput || ""); // Merge options with selected values that might not be in options // This ensures selected values are always visible and removable const mergedOptions = useMemo(() => { const optionSet = new Set(options.map((o) => o.value)); const missingSelectedOptions: FilterOption[] = values .filter((v) => !optionSet.has(v) && v.length > 0) .map((v) => ({ value: v })); return [...options, ...missingSelectedOptions]; }, [options, values]); const selectableOptions = useMemo( () => mergedOptions.filter((option) => option.value.length > 0), [mergedOptions], ); const allSelectedState = useMemo(() => { if (selectableOptions.length === 0) return false; return selectableOptions.every((option) => selectedValues.has(option.value), ); }, [selectableOptions, selectedValues]); const handleSelectAll = useCallback(() => { const newSelectedValues = new Set(selectedValues); if (allSelectedState) { // Deselect all selectable options selectableOptions.forEach((option) => newSelectedValues.delete(option.value), ); } else { // Select all selectable options selectableOptions.forEach((option) => newSelectedValues.add(option.value), ); } const filterValues = Array.from(newSelectedValues); onValueChange(filterValues.length ? filterValues : []); }, [allSelectedState, selectableOptions, selectedValues, onValueChange]); const debounceTimeout = useRef(null); const handleDebouncedChange = (value: string) => { const freeTextInput = getFreeTextInput( isCustomSelectEnabled, values, optionValues, ); if (!!freeTextInput) { selectedValues.delete(freeTextInput); selectedValues.add(value); selectedValues.delete(""); const filterValues = Array.from(selectedValues); onValueChange(filterValues.length ? filterValues : []); } }; function getSelectedOptions() { const selectedOptions = options.filter(({ value }) => selectedValues.has(value), ); const hasCustomOption = !!freeText && !!getFreeTextInput(isCustomSelectEnabled, values, optionValues); const customOption: FilterOption[] = hasCustomOption ? [{ value: freeText }] : []; return [...selectedOptions, ...customOption]; } return ( {/* if isCustomSelectEnabled we always show custom select hence never empty */} {!isCustomSelectEnabled && ( No results found. )} {selectableOptions.length > 0 && ( <>
{allSelectedState ? "Deselect All" : "Select All"}
)} {mergedOptions.map((option) => { if (option.value.length === 0) return; const isSelected = selectedValues.has(option.value); const displayValue = option.displayValue ?? (option.value === "" ? "(empty)" : option.value); const displayTitle = option.displayValue ?? (option.value === "" ? "(empty)" : option.value); const commandItem = ( { if (isSelected) { selectedValues.delete(option.value); } else { selectedValues.add(option.value); } const filterValues = Array.from(selectedValues); onValueChange(filterValues.length ? filterValues : []); }} >
{displayValue}
{option.count !== undefined ? ( {option.count} ) : null}
); return option.description ? ( {commandItem} ) : ( commandItem ); })}
{isCustomSelectEnabled && ( { const freeTextInput = getFreeTextInput( isCustomSelectEnabled, values, optionValues, ); if (!!freeTextInput) { selectedValues.delete(freeTextInput); } else { selectedValues.add(freeText); } selectedValues.delete(""); const filterValues = Array.from(selectedValues); onValueChange(filterValues.length ? filterValues : []); }} >
{ setFreeText(e.target.value); if (debounceTimeout.current) { clearTimeout(debounceTimeout.current); } debounceTimeout.current = setTimeout(() => { handleDebouncedChange(e.target.value); }, 500); }} onClick={(e) => { e.stopPropagation(); }} placeholder="Enter custom value" className="h-6 w-full rounded-none border-b-2 border-l-0 border-r-0 border-t-0 border-dotted p-0 text-sm" />
)} {selectedValues.size > 0 && ( <> onValueChange([])} className="justify-center text-center" > Clear filters )}
); }