Feat: Add the SelectWithSearch component #3221 (#7892)

### What problem does this PR solve?

Feat: Add the SelectWithSearch component #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-05-28 09:22:09 +08:00 committed by GitHub
parent 273f36cc54
commit 84f5ae20be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 399 additions and 143 deletions

View File

@ -7,7 +7,7 @@ import {
} from '@/components/ui/form';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { NextLLMSelect } from './llm-select';
import { NextLLMSelect } from './llm-select/next';
export function LargeModelFormField() {
const form = useFormContext();

View File

@ -1,12 +1,7 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Popover as AntPopover, Select as AntSelect } from 'antd';
import { forwardRef, useState } from 'react';
import LlmSettingItems from '../llm-setting-items';
import { LlmSettingFieldItems } from '../llm-setting-items/next';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Select, SelectTrigger, SelectValue } from '../ui/select';
interface IProps {
id?: string;
@ -16,7 +11,13 @@ interface IProps {
disabled?: boolean;
}
const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) => {
const LLMSelect = ({
id,
value,
onInitialValue,
onChange,
disabled,
}: IProps) => {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
@ -31,11 +32,12 @@ const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) =>
}
}
}
}
}
const content = (
<div style={{ width: 400 }}>
<LlmSettingItems onChange={onChange}
<LlmSettingItems
onChange={onChange}
formItemLayout={{ labelCol: { span: 10 }, wrapperCol: { span: 14 } }}
></LlmSettingItems>
</div>
@ -63,43 +65,3 @@ const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) =>
};
export default LLMSelect;
export const NextLLMSelect = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
IProps
>(({ value, disabled }, ref) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
return (
<Select disabled={disabled} value={value}>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<SelectTrigger
onClick={(e) => {
e.preventDefault();
setIsPopoverOpen(true);
}}
ref={ref}
>
<SelectValue>
{
modelOptions
.flatMap((x) => x.options)
.find((x) => x.value === value)?.label
}
</SelectValue>
</SelectTrigger>
</PopoverTrigger>
<PopoverContent side={'left'}>
<LlmSettingFieldItems></LlmSettingFieldItems>
</PopoverContent>
</Popover>
</Select>
);
});
NextLLMSelect.displayName = 'LLMSelect';

View File

@ -0,0 +1,55 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import * as SelectPrimitive from '@radix-ui/react-select';
import { forwardRef, useState } from 'react';
import { LlmSettingFieldItems } from '../llm-setting-items/next';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Select, SelectTrigger, SelectValue } from '../ui/select';
interface IProps {
id?: string;
value?: string;
onInitialValue?: (value: string, option: any) => void;
onChange?: (value: string, option: any) => void;
disabled?: boolean;
}
export const NextLLMSelect = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
IProps
>(({ value, disabled }, ref) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
return (
<Select disabled={disabled} value={value}>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<SelectTrigger
onClick={(e) => {
e.preventDefault();
setIsPopoverOpen(true);
}}
ref={ref}
>
<SelectValue>
{
modelOptions
.flatMap((x) => x.options)
.find((x) => x.value === value)?.label
}
</SelectValue>
</SelectTrigger>
</PopoverTrigger>
<PopoverContent side={'left'}>
<LlmSettingFieldItems></LlmSettingFieldItems>
</PopoverContent>
</Popover>
</Select>
);
});
NextLLMSelect.displayName = 'LLMSelect';

View File

@ -4,6 +4,7 @@ import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import { camelCase } from 'lodash';
import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
import {
FormControl,
FormField,
@ -11,7 +12,6 @@ import {
FormLabel,
FormMessage,
} from '../ui/form';
import { Input } from '../ui/input';
import {
Select,
SelectContent,
@ -21,86 +21,20 @@ import {
SelectTrigger,
SelectValue,
} from '../ui/select';
import { FormSlider } from '../ui/slider';
import { Switch } from '../ui/switch';
interface SliderWithInputNumberFormFieldProps {
name: string;
label: string;
checkName: string;
max: number;
min?: number;
step?: number;
}
function SliderWithInputNumberFormField({
name,
label,
checkName,
max,
min = 0,
step = 1,
}: SliderWithInputNumberFormFieldProps) {
const { control, watch } = useFormContext();
const { t } = useTranslate('chat');
const disabled = !watch(checkName);
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>{t(label)}</FormLabel>
<FormField
control={control}
name={checkName}
render={({ field }) => (
<FormItem>
<FormControl>
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormControl>
<div className="flex w-full items-center space-x-2">
<FormSlider
{...field}
disabled={disabled}
max={max}
min={min}
step={step}
></FormSlider>
<Input
type={'number'}
className="w-2/5"
{...field}
disabled={disabled}
max={max}
min={min}
step={step}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
import { SliderInputSwitchFormField } from './slider';
interface LlmSettingFieldItemsProps {
prefix?: string;
}
export const LlmSettingSchema = {
llm_id: z.string(),
temperature: z.coerce.number(),
top_p: z.string(),
presence_penalty: z.coerce.number(),
frequency_penalty: z.coerce.number(),
};
export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) {
const form = useFormContext();
const { t } = useTranslate('chat');
@ -122,7 +56,7 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) {
);
return (
<div className="space-y-8">
<div className="space-y-5">
<FormField
control={form.control}
name={'llm_id'}
@ -180,40 +114,40 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) {
</FormItem>
)}
/>
<SliderWithInputNumberFormField
<SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')}
checkName="temperatureEnabled"
label="temperature"
max={1}
step={0.01}
></SliderWithInputNumberFormField>
<SliderWithInputNumberFormField
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')}
checkName="topPEnabled"
label="topP"
max={1}
step={0.01}
></SliderWithInputNumberFormField>
<SliderWithInputNumberFormField
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')}
checkName="presencePenaltyEnabled"
label="presencePenalty"
max={1}
step={0.01}
></SliderWithInputNumberFormField>
<SliderWithInputNumberFormField
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')}
checkName="frequencyPenaltyEnabled"
label="frequencyPenalty"
max={1}
step={0.01}
></SliderWithInputNumberFormField>
<SliderWithInputNumberFormField
></SliderInputSwitchFormField>
<SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled"
label="maxTokens"
max={128000}
></SliderWithInputNumberFormField>
></SliderInputSwitchFormField>
</div>
);
}

View File

@ -0,0 +1,92 @@
import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { useFormContext } from 'react-hook-form';
import { SingleFormSlider } from '../ui/dual-range-slider';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form';
import { Input } from '../ui/input';
import { Switch } from '../ui/switch';
type SliderInputSwitchFormFieldProps = {
max?: number;
min?: number;
step?: number;
name: string;
label: string;
defaultValue?: number;
className?: string;
checkName: string;
};
export function SliderInputSwitchFormField({
max,
min,
step,
label,
name,
defaultValue,
className,
checkName,
}: SliderInputSwitchFormFieldProps) {
const form = useFormContext();
const disabled = !form.watch(checkName);
const { t } = useTranslate('chat');
return (
<FormField
control={form.control}
name={name}
defaultValue={defaultValue}
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t(`${label}Tip`)}>{t(label)}</FormLabel>
<div
className={cn('flex items-center gap-4 justify-between', className)}
>
<FormField
control={form.control}
name={checkName}
render={({ field }) => (
<FormItem>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormControl>
<SingleFormSlider
{...field}
max={max}
min={min}
step={step}
disabled={disabled}
></SingleFormSlider>
</FormControl>
<FormControl>
<Input
disabled={disabled}
type={'number'}
className="h-7 w-20"
max={max}
min={min}
step={step}
{...field}
></Input>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -0,0 +1,163 @@
'use client';
import { CheckIcon, ChevronDownIcon } from 'lucide-react';
import { Fragment, useCallback, useEffect, useId, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RAGFlowSelectOptionType } from '../ui/select';
const countries = [
{
label: 'America',
options: [
{ value: 'United States', label: '🇺🇸' },
{ value: 'Canada', label: '🇨🇦' },
{ value: 'Mexico', label: '🇲🇽' },
],
},
{
label: 'Africa',
options: [
{ value: 'South Africa', label: '🇿🇦' },
{ value: 'Nigeria', label: '🇳🇬' },
{ value: 'Morocco', label: '🇲🇦' },
],
},
{
label: 'Asia',
options: [
{ value: 'China', label: '🇨🇳' },
{ value: 'Japan', label: '🇯🇵' },
{ value: 'India', label: '🇮🇳' },
],
},
{
label: 'Europe',
options: [
{ value: 'United Kingdom', label: '🇬🇧' },
{ value: 'France', label: '🇫🇷' },
{ value: 'Germany', label: '🇩🇪' },
],
},
{
label: 'Oceania',
options: [
{ value: 'Australia', label: '🇦🇺' },
{ value: 'New Zealand', label: '🇳🇿' },
],
},
];
export type SelectWithSearchFlagOptionType = {
label: string;
options: RAGFlowSelectOptionType[];
};
export type SelectWithSearchFlagProps = {
options?: SelectWithSearchFlagOptionType[];
value?: string;
onChange?(value: string): void;
};
export function SelectWithSearch({
value: val = '',
onChange,
options = countries,
}: SelectWithSearchFlagProps) {
const id = useId();
const [open, setOpen] = useState<boolean>(false);
const [value, setValue] = useState<string>('');
const handleSelect = useCallback(
(val: string) => {
setValue(val);
setOpen(false);
onChange?.(val);
},
[onChange],
);
useEffect(() => {
setValue(val);
}, [val]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
id={id}
variant="outline"
role="combobox"
aria-expanded={open}
className="bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]"
>
{value ? (
<span className="flex min-w-0 options-center gap-2">
<span className="text-lg leading-none truncate">
{
options
.map((group) =>
group.options.find((item) => item.value === value),
)
.filter(Boolean)[0]?.label
}
</span>
</span>
) : (
<span className="text-muted-foreground">Select value</span>
)}
<ChevronDownIcon
size={16}
className="text-muted-foreground/80 shrink-0"
aria-hidden="true"
/>
</Button>
</PopoverTrigger>
<PopoverContent
className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0"
align="start"
>
<Command>
<CommandInput placeholder="Search ..." />
<CommandList>
<CommandEmpty>No data found.</CommandEmpty>
{options.map((group) => (
<Fragment key={group.label}>
<CommandGroup heading={group.label}>
{group.options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={handleSelect}
>
<span className="text-lg leading-none">
{option.label}
</span>
{option.value}
{value === option.value && (
<CheckIcon size={16} className="ml-auto" />
)}
</CommandItem>
))}
</CommandGroup>
</Fragment>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@ -1,3 +1,4 @@
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
@ -116,6 +117,7 @@ export function useFormConfigMap() {
component: CategorizeForm,
defaultValues: { message_history_window_size: 1 },
schema: z.object({
...LlmSettingSchema,
message_history_window_size: z.number(),
items: z.array(
z.object({

View File

@ -1,20 +1,45 @@
import { LargeModelFormField } from '@/components/large-model-form-field';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { Form } from '@/components/ui/form';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useTranslation } from 'react-i18next';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
import DynamicCategorize from './dynamic-categorize';
const CategorizeForm = ({ form, node }: INextOperatorForm) => {
const { t } = useTranslation();
return (
<Form {...form}>
<form
className="space-y-6 p-5 overflow-auto max-h-[76vh]"
className="space-y-6 p-5 "
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<FormField
control={form.control}
name="input"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.modelTip')}>
{t('chat.input')}
</FormLabel>
<FormControl>
<SelectWithSearch {...field}></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<LargeModelFormField></LargeModelFormField>
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>

View File

@ -1,4 +1,4 @@
import { NextLLMSelect } from '@/components/llm-select';
import { NextLLMSelect } from '@/components/llm-select/next';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { PromptEditor } from '@/components/prompt-editor';
import {

View File

@ -1,4 +1,4 @@
import { NextLLMSelect } from '@/components/llm-select';
import { NextLLMSelect } from '@/components/llm-select/next';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,

View File

@ -1,4 +1,4 @@
import { NextLLMSelect } from '@/components/llm-select';
import { NextLLMSelect } from '@/components/llm-select/next';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import {
Form,

View File

@ -158,3 +158,26 @@ export const downloadJsonFile = async (
const blob = new Blob([JSON.stringify(data)], { type: FileMimeType.Json });
downloadFileFromBlob(blob, fileName);
};
export function transformBase64ToFileWithPreview(
dataUrl: string,
filename: string = 'file',
) {
const file = transformBase64ToFile(dataUrl, filename);
(file as any).preview = dataUrl;
return file;
}
export const getBase64FromFileList = async (fileList?: File[]) => {
if (Array.isArray(fileList) && fileList.length > 0) {
const file = fileList[0];
if (file) {
const base64 = await transformFile2Base64(file);
return base64;
}
}
return '';
};