LibreChat/client/src/components/ui/Combobox.tsx
Danny Avila 3f600f0d3f
⬇️ fix: JSON LibreChat Imports (#2897)
* chore: remove unused code

* refactor: Update NewChatButtonIcon component to use JSX syntax

The NewChatButtonIcon component in the Nav folder has been updated to use JSX syntax instead of calling the Icon function directly. This change improves code readability and maintainability.

* remove use memo

* refactor: allow passing `select` to messages db query

* fix: initial fix for non-recursive messages

* ci: first pass, importers test rewrite

* fix(groupConversationsByDate): handle edge case of conversation.updatedAt being null

* fix: correctly handle non-recursive uploads

* feat: imports non-recursive conversations with branches correctly

* feat: support retaining original options on import

* refactor: Allow `messageTree` field for Import of non-recursive conversations
2024-05-29 09:15:05 -04:00

162 lines
7.4 KiB
TypeScript

import { startTransition, useMemo } from 'react';
import { Search as SearchIcon } from 'lucide-react';
import * as RadixSelect from '@radix-ui/react-select';
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
import {
Combobox,
ComboboxItem,
ComboboxList,
ComboboxProvider,
ComboboxCancel,
} from '@ariakit/react';
import type { OptionWithIcon } from '~/common';
import { SelectTrigger, SelectValue, SelectScrollDownButton } from './Select';
import useCombobox from '~/hooks/Input/useCombobox';
import { cn } from '~/utils';
export default function ComboboxComponent({
selectedValue,
displayValue,
items,
setValue,
ariaLabel,
searchPlaceholder,
selectPlaceholder,
isCollapsed,
SelectIcon,
}: {
ariaLabel: string;
displayValue?: string;
selectedValue: string;
searchPlaceholder?: string;
selectPlaceholder?: string;
items: OptionWithIcon[] | string[];
setValue: (value: string) => void;
isCollapsed: boolean;
SelectIcon?: React.ReactNode;
}) {
const options: OptionWithIcon[] = (items ?? []).map((option: string | OptionWithIcon) => {
if (typeof option === 'string') {
return { label: option, value: option };
}
return option;
});
const { open, setOpen, setSearchValue, matches } = useCombobox({
value: selectedValue,
options,
});
return (
<RadixSelect.Root
value={selectedValue}
onValueChange={setValue}
open={open}
onOpenChange={setOpen}
>
<ComboboxProvider
open={open}
setOpen={setOpen}
resetValueOnHide
includesBaseElement={false}
setValue={(value) => {
startTransition(() => {
setSearchValue(value);
});
}}
>
<SelectTrigger
aria-label={ariaLabel}
className={cn(
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
isCollapsed
? 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
: '',
'bg-white text-black hover:bg-gray-50 dark:bg-gray-850 dark:text-white',
)}
>
<SelectValue placeholder={selectPlaceholder}>
<div className="assistant-item flex items-center justify-center overflow-hidden rounded-full">
{SelectIcon ? SelectIcon : <ChevronDownIcon />}
</div>
<span
className={cn('ml-2', isCollapsed ? 'hidden' : '')}
style={{ userSelect: 'none' }}
>
{selectedValue
? displayValue ?? selectedValue
: selectPlaceholder && selectPlaceholder}
</span>
</SelectValue>
</SelectTrigger>
<RadixSelect.Portal>
<RadixSelect.Content
role="dialog"
aria-label={ariaLabel + 's'}
position="popper"
className={cn(
'bg-popover text-popover-foreground relative z-50 max-h-[52vh] min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
'bg-white dark:bg-gray-700',
)}
>
<RadixSelect.Viewport className="mb-5 h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]">
<div className="group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-white px-2 px-3 py-2 text-black duration-300 dark:bg-gray-700 dark:text-white">
<SearchIcon className="h-4 w-4 text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300" />
<Combobox
autoSelect
placeholder={searchPlaceholder}
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-gray-700/10 dark:focus:ring-gray-200/10"
// Ariakit's Combobox manually triggers a blur event on virtually
// blurred items, making them work as if they had actual DOM
// focus. These blur events might happen after the corresponding
// focus events in the capture phase, leading Radix Select to
// close the popover. This happens because Radix Select relies on
// the order of these captured events to discern if the focus was
// outside the element. Since we don't have access to the
// onInteractOutside prop in the Radix SelectContent component to
// stop this behavior, we can turn off Ariakit's behavior here.
onBlurCapture={(event) => {
event.preventDefault();
event.stopPropagation();
}}
/>
<ComboboxCancel
hideWhenEmpty={true}
className="relative flex h-5 w-5 items-center justify-end text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300"
/>
</div>
<ComboboxList className="overflow-y-auto p-1 py-2">
{matches.map(({ label, value, icon }) => (
<RadixSelect.Item key={value} value={`${value ?? ''}`} asChild>
<ComboboxItem
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'rounded-lg hover:bg-gray-100/50 hover:bg-gray-50 dark:text-white dark:hover:bg-gray-600',
)}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<RadixSelect.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</RadixSelect.ItemIndicator>
</span>
<RadixSelect.ItemText>
<div className="[&_svg]:text-foreground flex items-center justify-center gap-3 dark:text-white [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0">
<div className="assistant-item overflow-hidden rounded-full ">
{icon && icon}
</div>
{label}
</div>
</RadixSelect.ItemText>
</ComboboxItem>
</RadixSelect.Item>
))}
</ComboboxList>
</RadixSelect.Viewport>
<SelectScrollDownButton className="absolute bottom-0 left-0 right-0" />
</RadixSelect.Content>
</RadixSelect.Portal>
</ComboboxProvider>
</RadixSelect.Root>
);
}