From ad77f504f96dc646fab36f234cc1c3aedbf2b85f Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 25 Jul 2025 19:25:19 +0800 Subject: [PATCH] Feat: Filter the agent form's large model list by type #3221 (#9049) ### What problem does this PR solve? Feat: Filter the agent form's large model list by type #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/components/large-model-form-field.tsx | 95 ++++++- web/src/components/llm-select/next.tsx | 14 +- web/src/components/llm-setting-items/next.tsx | 30 +-- .../originui/select-with-search.tsx | 255 ++++++++++-------- web/src/hooks/use-llm-request.ts | 43 +++ web/src/pages/agent/form/agent-form/index.tsx | 31 ++- .../agent/form/components/query-variable.tsx | 1 + web/src/pages/agent/index.tsx | 26 +- web/src/utils/llm-util.ts | 6 + 9 files changed, 331 insertions(+), 170 deletions(-) create mode 100644 web/src/hooks/use-llm-request.ts diff --git a/web/src/components/large-model-form-field.tsx b/web/src/components/large-model-form-field.tsx index 58d58f385..5647c6864 100644 --- a/web/src/components/large-model-form-field.tsx +++ b/web/src/components/large-model-form-field.tsx @@ -1,3 +1,9 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { FormControl, FormField, @@ -5,27 +11,88 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; -import { useFormContext } from 'react-hook-form'; +import { LlmModelType } from '@/constants/knowledge'; +import { Funnel } from 'lucide-react'; +import { useFormContext, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; import { NextLLMSelect } from './llm-select/next'; +import { Button } from './ui/button'; + +const ModelTypes = [ + { + title: 'All Models', + value: 'all', + }, + { + title: 'Text-only Models', + value: LlmModelType.Chat, + }, + { + title: 'Multimodal Models', + value: LlmModelType.Image2text, + }, +]; + +export const LargeModelFilterFormSchema = { + llm_filter: z.string().optional(), +}; export function LargeModelFormField() { const form = useFormContext(); const { t } = useTranslation(); + const filter = useWatch({ control: form.control, name: 'llm_filter' }); return ( - ( - - {t('chat.model')} - - - - - - )} - /> + <> + ( + + + {t('chat.model')} + +
+ ( + + + + + + + + {ModelTypes.map((x) => ( + { + field.onChange(x.value); + }} + > + {x.title} + + ))} + + + + + )} + /> + + + + +
+ + +
+ )} + /> + ); } diff --git a/web/src/components/llm-select/next.tsx b/web/src/components/llm-select/next.tsx index 075b43e5a..45fc45e24 100644 --- a/web/src/components/llm-select/next.tsx +++ b/web/src/components/llm-select/next.tsx @@ -12,17 +12,19 @@ interface IProps { onInitialValue?: (value: string, option: any) => void; onChange?: (value: string) => void; disabled?: boolean; + filter?: string; } const NextInnerLLMSelect = forwardRef< React.ElementRef, IProps ->(({ value, disabled }, ref) => { +>(({ value, disabled, filter }, ref) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const modelOptions = useComposeLlmOptionsByModelTypes([ - LlmModelType.Chat, - LlmModelType.Image2text, - ]); + const modelTypes = + filter === 'all' || filter === undefined + ? [LlmModelType.Chat, LlmModelType.Image2text] + : [filter as LlmModelType]; + const modelOptions = useComposeLlmOptionsByModelTypes(modelTypes); return ( diff --git a/web/src/components/llm-setting-items/next.tsx b/web/src/components/llm-setting-items/next.tsx index 58c5f9fac..ac582657c 100644 --- a/web/src/components/llm-setting-items/next.tsx +++ b/web/src/components/llm-setting-items/next.tsx @@ -25,6 +25,7 @@ import { useHandleFreedomChange } from './use-watch-change'; interface LlmSettingFieldItemsProps { prefix?: string; + options?: any[]; } export const LlmSettingSchema = { @@ -40,9 +41,13 @@ export const LlmSettingSchema = { maxTokensEnabled: z.boolean(), }; -export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { +export function LlmSettingFieldItems({ + prefix, + options, +}: LlmSettingFieldItemsProps) { const form = useFormContext(); const { t } = useTranslate('chat'); + const modelOptions = useComposeLlmOptionsByModelTypes([ LlmModelType.Chat, LlmModelType.Image2text, @@ -72,30 +77,9 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { {t('model')} - {/* */} diff --git a/web/src/components/originui/select-with-search.tsx b/web/src/components/originui/select-with-search.tsx index ce4327332..13125dd63 100644 --- a/web/src/components/originui/select-with-search.tsx +++ b/web/src/components/originui/select-with-search.tsx @@ -1,8 +1,9 @@ 'use client'; -import { CheckIcon, ChevronDownIcon } from 'lucide-react'; +import { CheckIcon, ChevronDownIcon, XIcon } from 'lucide-react'; import { Fragment, + MouseEventHandler, ReactNode, forwardRef, useCallback, @@ -28,6 +29,7 @@ import { } from '@/components/ui/popover'; import { cn } from '@/lib/utils'; import { RAGFlowSelectOptionType } from '../ui/select'; +import { Separator } from '../ui/separator'; export type SelectWithSearchFlagOptionType = { label: ReactNode; @@ -41,122 +43,159 @@ export type SelectWithSearchFlagProps = { value?: string; onChange?(value: string): void; triggerClassName?: string; + allowClear?: boolean; }; export const SelectWithSearch = forwardRef< React.ElementRef, SelectWithSearchFlagProps ->(({ value: val = '', onChange, options = [], triggerClassName }, ref) => { - const id = useId(); - const [open, setOpen] = useState(false); - const [value, setValue] = useState(''); - - const handleSelect = useCallback( - (val: string) => { - setValue(val); - setOpen(false); - onChange?.(val); +>( + ( + { + value: val = '', + onChange, + options = [], + triggerClassName, + allowClear = false, }, - [onChange], - ); + ref, + ) => { + const id = useId(); + const [open, setOpen] = useState(false); + const [value, setValue] = useState(''); - useEffect(() => { - setValue(val); - }, [val]); - const selectLabel = useMemo(() => { - const optionTemp = options[0]; - if (optionTemp?.options) { - return options - .map((group) => group?.options?.find((item) => item.value === value)) - .filter(Boolean)[0]?.label; - } else { - return options.find((opt) => opt.value === value)?.label || ''; - } - }, [options, value]); - return ( - - - - - - - - - No data found. - {options.map((group, idx) => { - if (group.options) { - return ( - - - {group.options.map((option) => ( - - - {option.label} - + ) : ( + Select value + )} +
+ {value && allowClear && ( + <> + + + + )} +
+ + + + + + + No data found. + {options.map((group, idx) => { + if (group.options) { + return ( + + + {group.options.map((option) => ( + + + {option.label} + - {value === option.value && ( - - )} - - ))} - - - ); - } else { - return ( - - {group.label} + {value === option.value && ( + + )} + + ))} +
+
+ ); + } else { + return ( + + + {group.label} + - {value === group.value && ( - - )} - - ); - } - })} -
-
-
-
- ); -}); + {value === group.value && ( + + )} + + ); + } + })} + + + + + ); + }, +); SelectWithSearch.displayName = 'SelectWithSearch'; diff --git a/web/src/hooks/use-llm-request.ts b/web/src/hooks/use-llm-request.ts new file mode 100644 index 000000000..93970b63c --- /dev/null +++ b/web/src/hooks/use-llm-request.ts @@ -0,0 +1,43 @@ +import { LlmModelType } from '@/constants/knowledge'; +import userService from '@/services/user-service'; +import { useQuery } from '@tanstack/react-query'; + +import { + IThirdOAIModelCollection as IThirdAiModelCollection, + IThirdOAIModel, +} from '@/interfaces/database/llm'; +import { buildLlmUuid } from '@/utils/llm-util'; + +export const useFetchLlmList = (modelType?: LlmModelType) => { + const { data } = useQuery({ + queryKey: ['llmList'], + initialData: {}, + queryFn: async () => { + const { data } = await userService.llm_list({ model_type: modelType }); + + return data?.data ?? {}; + }, + }); + + return data; +}; + +type IThirdOAIModelWithUuid = IThirdOAIModel & { uuid: string }; + +export function useSelectFlatLlmList(modelType?: LlmModelType) { + const llmList = useFetchLlmList(modelType); + + return Object.values(llmList).reduce((pre, cur) => { + pre.push(...cur.map((x) => ({ ...x, uuid: buildLlmUuid(x) }))); + + return pre; + }, []); +} + +export function useFindLlmByUuid(modelType?: LlmModelType) { + const flatList = useSelectFlatLlmList(modelType); + + return (uuid: string) => { + return flatList.find((x) => x.uuid === uuid); + }; +} diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index 55c19cdc2..e406046ce 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/web/src/pages/agent/form/agent-form/index.tsx @@ -1,6 +1,9 @@ import { Collapse } from '@/components/collapse'; import { FormContainer } from '@/components/form-container'; -import { LargeModelFormField } from '@/components/large-model-form-field'; +import { + LargeModelFilterFormSchema, + LargeModelFormField, +} from '@/components/large-model-form-field'; import { LlmSettingSchema } from '@/components/llm-setting-items/next'; import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; import { @@ -12,10 +15,12 @@ import { } from '@/components/ui/form'; import { Input, NumberInput } from '@/components/ui/input'; import { RAGFlowSelect } from '@/components/ui/select'; +import { LlmModelType } from '@/constants/knowledge'; +import { useFindLlmByUuid } from '@/hooks/use-llm-request'; import { buildOptions } from '@/utils/form'; import { zodResolver } from '@hookform/resolvers/zod'; import { memo, useMemo } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import { @@ -65,6 +70,7 @@ const FormSchema = z.object({ exception_method: z.string().nullable(), exception_comment: z.string().optional(), exception_goto: z.string().optional(), + ...LargeModelFilterFormSchema, }); function AgentForm({ node }: INextOperatorForm) { @@ -88,6 +94,10 @@ function AgentForm({ node }: INextOperatorForm) { resolver: zodResolver(FormSchema), }); + const llmId = useWatch({ control: form.control, name: 'llm_id' }); + + const findLlmByUuid = useFindLlmByUuid(); + useWatchFormChange(node?.id, form); return ( @@ -101,6 +111,16 @@ function AgentForm({ node }: INextOperatorForm) { {isSubAgent && } + {findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && ( + + )} + + + )} /> - {isSubAgent || ( @@ -148,11 +167,7 @@ function AgentForm({ node }: INextOperatorForm) { Advanced Settings}> - + diff --git a/web/src/pages/agent/index.tsx b/web/src/pages/agent/index.tsx index cced9b469..f7d0326ae 100644 --- a/web/src/pages/agent/index.tsx +++ b/web/src/pages/agent/index.tsx @@ -161,17 +161,21 @@ export default function Agent() { {t('flow.export')} - - - - {t('common.embedIntoSite')} - + {location.hostname !== 'demo.ragflow.io' && ( + <> + + + + {t('common.embedIntoSite')} + + + )} diff --git a/web/src/utils/llm-util.ts b/web/src/utils/llm-util.ts index a2e875a83..6935096b5 100644 --- a/web/src/utils/llm-util.ts +++ b/web/src/utils/llm-util.ts @@ -1,3 +1,5 @@ +import { IThirdOAIModel } from '@/interfaces/database/llm'; + export const getLLMIconName = (fid: string, llm_name: string) => { if (fid === 'FastEmbed') { return llm_name.split('/').at(0) ?? ''; @@ -16,3 +18,7 @@ export const getLlmNameAndFIdByLlmId = (llmId?: string) => { export function getRealModelName(llmName: string) { return llmName.split('__').at(0) ?? ''; } + +export function buildLlmUuid(llm: IThirdOAIModel) { + return `${llm.llm_name}@${llm.fid}`; +}