From 84f5ae20be7088a95e57ba155d15e32a952fe9ae Mon Sep 17 00:00:00 2001 From: balibabu Date: Wed, 28 May 2025 09:22:09 +0800 Subject: [PATCH] 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) --- web/src/components/large-model-form-field.tsx | 2 +- web/src/components/llm-select/index.tsx | 58 ++----- web/src/components/llm-select/next.tsx | 55 ++++++ web/src/components/llm-setting-items/next.tsx | 108 +++--------- .../components/llm-setting-items/slider.tsx | 92 ++++++++++ .../originui/select-with-search.tsx | 163 ++++++++++++++++++ .../agent/form-sheet/use-form-config-map.tsx | 2 + .../agent/form/categorize-form/index.tsx | 33 +++- .../pages/agent/form/generate-form/index.tsx | 2 +- .../agent/form/keyword-extract-form/index.tsx | 2 +- .../form/rewrite-question-form/index.tsx | 2 +- web/src/utils/file-util.ts | 23 +++ 12 files changed, 399 insertions(+), 143 deletions(-) create mode 100644 web/src/components/llm-select/next.tsx create mode 100644 web/src/components/llm-setting-items/slider.tsx create mode 100644 web/src/components/originui/select-with-search.tsx diff --git a/web/src/components/large-model-form-field.tsx b/web/src/components/large-model-form-field.tsx index f6b2a4314..58d58f385 100644 --- a/web/src/components/large-model-form-field.tsx +++ b/web/src/components/large-model-form-field.tsx @@ -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(); diff --git a/web/src/components/llm-select/index.tsx b/web/src/components/llm-select/index.tsx index fc31f3a6c..fa65b95c3 100644 --- a/web/src/components/llm-select/index.tsx +++ b/web/src/components/llm-select/index.tsx @@ -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 = (
-
@@ -63,43 +65,3 @@ const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) => }; export default LLMSelect; - -export const NextLLMSelect = forwardRef< - React.ElementRef, - IProps ->(({ value, disabled }, ref) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const modelOptions = useComposeLlmOptionsByModelTypes([ - LlmModelType.Chat, - LlmModelType.Image2text, - ]); - - return ( - - ); -}); - -NextLLMSelect.displayName = 'LLMSelect'; diff --git a/web/src/components/llm-select/next.tsx b/web/src/components/llm-select/next.tsx new file mode 100644 index 000000000..9e1141a59 --- /dev/null +++ b/web/src/components/llm-select/next.tsx @@ -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, + IProps +>(({ value, disabled }, ref) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const modelOptions = useComposeLlmOptionsByModelTypes([ + LlmModelType.Chat, + LlmModelType.Image2text, + ]); + + return ( + + ); +}); + +NextLLMSelect.displayName = 'LLMSelect'; diff --git a/web/src/components/llm-setting-items/next.tsx b/web/src/components/llm-setting-items/next.tsx index 31e63068b..351916d86 100644 --- a/web/src/components/llm-setting-items/next.tsx +++ b/web/src/components/llm-setting-items/next.tsx @@ -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 ( - ( - -
- {t(label)} - ( - - - - - - - )} - /> -
- -
- - -
-
- -
- )} - /> - ); -} +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 ( -
+
)} /> - - + - + - + - + + >
); } diff --git a/web/src/components/llm-setting-items/slider.tsx b/web/src/components/llm-setting-items/slider.tsx new file mode 100644 index 000000000..a9137b353 --- /dev/null +++ b/web/src/components/llm-setting-items/slider.tsx @@ -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 ( + ( + + {t(label)} +
+ ( + + + + + + + )} + /> + + + + + + +
+ +
+ )} + /> + ); +} diff --git a/web/src/components/originui/select-with-search.tsx b/web/src/components/originui/select-with-search.tsx new file mode 100644 index 000000000..c5b0c853e --- /dev/null +++ b/web/src/components/originui/select-with-search.tsx @@ -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(false); + const [value, setValue] = useState(''); + + const handleSelect = useCallback( + (val: string) => { + setValue(val); + setOpen(false); + onChange?.(val); + }, + [onChange], + ); + + useEffect(() => { + setValue(val); + }, [val]); + + return ( + + + + + + + + + No data found. + {options.map((group) => ( + + + {group.options.map((option) => ( + + + {option.label} + + {option.value} + {value === option.value && ( + + )} + + ))} + + + ))} + + + + + ); +} diff --git a/web/src/pages/agent/form-sheet/use-form-config-map.tsx b/web/src/pages/agent/form-sheet/use-form-config-map.tsx index e3175ac65..5d075e393 100644 --- a/web/src/pages/agent/form-sheet/use-form-config-map.tsx +++ b/web/src/pages/agent/form-sheet/use-form-config-map.tsx @@ -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({ diff --git a/web/src/pages/agent/form/categorize-form/index.tsx b/web/src/pages/agent/form/categorize-form/index.tsx index 220c17ded..eb1067fc0 100644 --- a/web/src/pages/agent/form/categorize-form/index.tsx +++ b/web/src/pages/agent/form/categorize-form/index.tsx @@ -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 (
{ e.preventDefault(); }} > - + ( + + + {t('chat.input')} + + + + + + + )} + /> + diff --git a/web/src/pages/agent/form/generate-form/index.tsx b/web/src/pages/agent/form/generate-form/index.tsx index d463e682c..d463c7114 100644 --- a/web/src/pages/agent/form/generate-form/index.tsx +++ b/web/src/pages/agent/form/generate-form/index.tsx @@ -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 { diff --git a/web/src/pages/agent/form/keyword-extract-form/index.tsx b/web/src/pages/agent/form/keyword-extract-form/index.tsx index 5ec092a35..bda5d44f5 100644 --- a/web/src/pages/agent/form/keyword-extract-form/index.tsx +++ b/web/src/pages/agent/form/keyword-extract-form/index.tsx @@ -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, diff --git a/web/src/pages/agent/form/rewrite-question-form/index.tsx b/web/src/pages/agent/form/rewrite-question-form/index.tsx index b2038a666..8047b7321 100644 --- a/web/src/pages/agent/form/rewrite-question-form/index.tsx +++ b/web/src/pages/agent/form/rewrite-question-form/index.tsx @@ -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, diff --git a/web/src/utils/file-util.ts b/web/src/utils/file-util.ts index 0a031253d..6d8ef9a43 100644 --- a/web/src/utils/file-util.ts +++ b/web/src/utils/file-util.ts @@ -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 ''; +};