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'; } from '@/components/ui/form';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { NextLLMSelect } from './llm-select'; import { NextLLMSelect } from './llm-select/next';
export function LargeModelFormField() { export function LargeModelFormField() {
const form = useFormContext(); const form = useFormContext();

View File

@ -1,12 +1,7 @@
import { LlmModelType } from '@/constants/knowledge'; import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks'; import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import * as SelectPrimitive from '@radix-ui/react-select';
import { Popover as AntPopover, Select as AntSelect } from 'antd'; import { Popover as AntPopover, Select as AntSelect } from 'antd';
import { forwardRef, useState } from 'react';
import LlmSettingItems from '../llm-setting-items'; 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 { interface IProps {
id?: string; id?: string;
@ -16,7 +11,13 @@ interface IProps {
disabled?: boolean; disabled?: boolean;
} }
const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) => { const LLMSelect = ({
id,
value,
onInitialValue,
onChange,
disabled,
}: IProps) => {
const modelOptions = useComposeLlmOptionsByModelTypes([ const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat, LlmModelType.Chat,
LlmModelType.Image2text, LlmModelType.Image2text,
@ -35,7 +36,8 @@ const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) =>
const content = ( const content = (
<div style={{ width: 400 }}> <div style={{ width: 400 }}>
<LlmSettingItems onChange={onChange} <LlmSettingItems
onChange={onChange}
formItemLayout={{ labelCol: { span: 10 }, wrapperCol: { span: 14 } }} formItemLayout={{ labelCol: { span: 10 }, wrapperCol: { span: 14 } }}
></LlmSettingItems> ></LlmSettingItems>
</div> </div>
@ -63,43 +65,3 @@ const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) =>
}; };
export default LLMSelect; 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 { camelCase } from 'lodash';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { import {
FormControl, FormControl,
FormField, FormField,
@ -11,7 +12,6 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from '../ui/form'; } from '../ui/form';
import { Input } from '../ui/input';
import { import {
Select, Select,
SelectContent, SelectContent,
@ -21,86 +21,20 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '../ui/select'; } from '../ui/select';
import { FormSlider } from '../ui/slider'; import { SliderInputSwitchFormField } from './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>
)}
/>
);
}
interface LlmSettingFieldItemsProps { interface LlmSettingFieldItemsProps {
prefix?: string; 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) { export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) {
const form = useFormContext(); const form = useFormContext();
const { t } = useTranslate('chat'); const { t } = useTranslate('chat');
@ -122,7 +56,7 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) {
); );
return ( return (
<div className="space-y-8"> <div className="space-y-5">
<FormField <FormField
control={form.control} control={form.control}
name={'llm_id'} name={'llm_id'}
@ -180,40 +114,40 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) {
</FormItem> </FormItem>
)} )}
/> />
<SliderWithInputNumberFormField <SliderInputSwitchFormField
name={getFieldWithPrefix('temperature')} name={getFieldWithPrefix('temperature')}
checkName="temperatureEnabled" checkName="temperatureEnabled"
label="temperature" label="temperature"
max={1} max={1}
step={0.01} step={0.01}
></SliderWithInputNumberFormField> ></SliderInputSwitchFormField>
<SliderWithInputNumberFormField <SliderInputSwitchFormField
name={getFieldWithPrefix('top_p')} name={getFieldWithPrefix('top_p')}
checkName="topPEnabled" checkName="topPEnabled"
label="topP" label="topP"
max={1} max={1}
step={0.01} step={0.01}
></SliderWithInputNumberFormField> ></SliderInputSwitchFormField>
<SliderWithInputNumberFormField <SliderInputSwitchFormField
name={getFieldWithPrefix('presence_penalty')} name={getFieldWithPrefix('presence_penalty')}
checkName="presencePenaltyEnabled" checkName="presencePenaltyEnabled"
label="presencePenalty" label="presencePenalty"
max={1} max={1}
step={0.01} step={0.01}
></SliderWithInputNumberFormField> ></SliderInputSwitchFormField>
<SliderWithInputNumberFormField <SliderInputSwitchFormField
name={getFieldWithPrefix('frequency_penalty')} name={getFieldWithPrefix('frequency_penalty')}
checkName="frequencyPenaltyEnabled" checkName="frequencyPenaltyEnabled"
label="frequencyPenalty" label="frequencyPenalty"
max={1} max={1}
step={0.01} step={0.01}
></SliderWithInputNumberFormField> ></SliderInputSwitchFormField>
<SliderWithInputNumberFormField <SliderInputSwitchFormField
name={getFieldWithPrefix('max_tokens')} name={getFieldWithPrefix('max_tokens')}
checkName="maxTokensEnabled" checkName="maxTokensEnabled"
label="maxTokens" label="maxTokens"
max={128000} max={128000}
></SliderWithInputNumberFormField> ></SliderInputSwitchFormField>
</div> </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 { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
@ -116,6 +117,7 @@ export function useFormConfigMap() {
component: CategorizeForm, component: CategorizeForm,
defaultValues: { message_history_window_size: 1 }, defaultValues: { message_history_window_size: 1 },
schema: z.object({ schema: z.object({
...LlmSettingSchema,
message_history_window_size: z.number(), message_history_window_size: z.number(),
items: z.array( items: z.array(
z.object({ z.object({

View File

@ -1,20 +1,45 @@
import { LargeModelFormField } from '@/components/large-model-form-field'; import { LargeModelFormField } from '@/components/large-model-form-field';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; 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 { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
import DynamicCategorize from './dynamic-categorize'; import DynamicCategorize from './dynamic-categorize';
const CategorizeForm = ({ form, node }: INextOperatorForm) => { const CategorizeForm = ({ form, node }: INextOperatorForm) => {
const { t } = useTranslation();
return ( return (
<Form {...form}> <Form {...form}>
<form <form
className="space-y-6 p-5 overflow-auto max-h-[76vh]" className="space-y-6 p-5 "
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); 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> <LargeModelFormField></LargeModelFormField>
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize> <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 { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { PromptEditor } from '@/components/prompt-editor'; import { PromptEditor } from '@/components/prompt-editor';
import { 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 { TopNFormField } from '@/components/top-n-item';
import { import {
Form, 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 { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { import {
Form, Form,

View File

@ -158,3 +158,26 @@ export const downloadJsonFile = async (
const blob = new Blob([JSON.stringify(data)], { type: FileMimeType.Json }); const blob = new Blob([JSON.stringify(data)], { type: FileMimeType.Json });
downloadFileFromBlob(blob, fileName); 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 '';
};