diff --git a/api/core/entities/parameter_entities.py b/api/core/entities/parameter_entities.py index 36800bc263..b071bfa5b1 100644 --- a/api/core/entities/parameter_entities.py +++ b/api/core/entities/parameter_entities.py @@ -15,6 +15,11 @@ class CommonParameterType(StrEnum): MODEL_SELECTOR = "model-selector" TOOLS_SELECTOR = "array[tools]" + # Dynamic select parameter + # Once you are not sure about the available options until authorization is done + # eg: Select a Slack channel from a Slack workspace + DYNAMIC_SELECT = "dynamic-select" + # TOOL_SELECTOR = "tool-selector" diff --git a/api/core/plugin/entities/parameters.py b/api/core/plugin/entities/parameters.py index 895dd0d0fc..a011801006 100644 --- a/api/core/plugin/entities/parameters.py +++ b/api/core/plugin/entities/parameters.py @@ -35,6 +35,7 @@ class PluginParameterType(enum.StrEnum): APP_SELECTOR = CommonParameterType.APP_SELECTOR.value MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value + DYNAMIC_SELECT = CommonParameterType.DYNAMIC_SELECT.value # deprecated, should not use. SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 03047c0545..d2c28076ae 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -240,6 +240,7 @@ class ToolParameter(PluginParameter): FILES = PluginParameterType.FILES.value APP_SELECTOR = PluginParameterType.APP_SELECTOR.value MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value + DYNAMIC_SELECT = PluginParameterType.DYNAMIC_SELECT.value # deprecated, should not use. SYSTEM_FILES = PluginParameterType.SYSTEM_FILES.value diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index fa8730f698..44cb0522f6 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -1,10 +1,10 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react' import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid' import Badge from '../badge/index' -import { RiCheckLine } from '@remixicon/react' +import { RiCheckLine, RiLoader4Line } from '@remixicon/react' import { useTranslation } from 'react-i18next' import classNames from '@/utils/classnames' import { @@ -51,6 +51,8 @@ export type ISelectProps = { item: Item selected: boolean }) => React.ReactNode + isLoading?: boolean + onOpenChange?: (open: boolean) => void } const Select: FC = ({ className, @@ -178,17 +180,20 @@ const SimpleSelect: FC = ({ defaultValue = 1, disabled = false, onSelect, + onOpenChange, placeholder, optionWrapClassName, optionClassName, hideChecked, notClearable, renderOption, + isLoading = false, }) => { const { t } = useTranslation() const localPlaceholder = placeholder || t('common.placeholder.select') const [selectedItem, setSelectedItem] = useState(null) + useEffect(() => { let defaultSelect = null const existed = items.find((item: Item) => item.value === defaultValue) @@ -199,8 +204,10 @@ const SimpleSelect: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue]) + const listboxRef = useRef(null) + return ( - { if (!disabled) { @@ -212,10 +219,17 @@ const SimpleSelect: FC = ({
{renderTrigger && {renderTrigger(selectedItem)}} {!renderTrigger && ( - + { + // get data-open, use setTimeout to ensure the attribute is set + setTimeout(() => { + if (listboxRef.current) + onOpenChange?.(listboxRef.current.getAttribute('data-open') !== null) + }) + }} className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}> {selectedItem?.name ?? localPlaceholder} - {(selectedItem && !notClearable) + {isLoading ? + : (selectedItem && !notClearable) ? ( { @@ -237,7 +251,7 @@ const SimpleSelect: FC = ({ )} - {!disabled && ( + {(!disabled) && ( {items.map((item: Item) => ( void + onOpenChange?: (open: boolean) => void + isLoading?: boolean } const DEFAULT_SCHEMA = {} as CredentialFormSchema @@ -22,6 +24,8 @@ const ConstantField: FC = ({ readonly, value, onChange, + onOpenChange, + isLoading, }) => { const language = useLanguage() const placeholder = (schema as CredentialFormSchemaSelect).placeholder @@ -36,7 +40,7 @@ const ConstantField: FC = ({ return ( <> - {schema.type === FormTypeEnum.select && ( + {(schema.type === FormTypeEnum.select || schema.type === FormTypeEnum.dynamicSelect) && ( = ({ items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))} onSelect={item => handleSelectChange(item.value)} placeholder={placeholder?.[language] || placeholder?.en_US} + onOpenChange={onOpenChange} + isLoading={isLoading} /> )} {schema.type === FormTypeEnum.textNumber && ( diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index e9825cd44a..9f54128548 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -6,6 +6,7 @@ import { RiArrowDownSLine, RiCloseLine, RiErrorWarningFill, + RiLoader4Line, RiMoreLine, } from '@remixicon/react' import produce from 'immer' @@ -17,7 +18,7 @@ import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStruct import ConstantField from './constant-field' import cn from '@/utils/classnames' import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' -import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { BlockEnum } from '@/app/components/workflow/types' import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { Line3 } from '@/app/components/base/icons/src/public/common' @@ -68,6 +69,7 @@ type Props = { minWidth?: number popupFor?: 'assigned' | 'toAssigned' zIndex?: number + isLoading?: boolean } const DEFAULT_VALUE_SELECTOR: Props['value'] = [] @@ -316,6 +318,50 @@ const VarReferencePicker: FC = ({ return null }, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type]) + + const [dynamicOptions, setDynamicOptions] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const handleOpenDynamicSelect = useCallback((open: boolean) => { + if (schema?.type !== FormTypeEnum.dynamicSelect || !open || isLoading) + return + if (dynamicOptions) + return + + setIsLoading(true) + // load options + setTimeout(() => { + setIsLoading(false) + setDynamicOptions([{ + label: { + en_US: 'Option 1', + zh_Hans: '选项1', + }, + value: 'option1', + show_on: [], + }, { + label: { + en_US: 'Option 2', + zh_Hans: '选项2', + }, + value: 'option2', + show_on: [], + }]) + }, 1000) + }, [isLoading]) + + const schemaWithDynamicSelect = useMemo(() => { + if (schema?.type !== FormTypeEnum.dynamicSelect) + return schema + // rewrite schema.options with dynamicOptions + if (dynamicOptions) { + return { + ...schema, + options: dynamicOptions, + } + } + return schema + }, [dynamicOptions]) + return (
= ({ void)} - schema={schema as CredentialFormSchema} + schema={schemaWithDynamicSelect as CredentialFormSchema} readonly={readonly} + onOpenChange={handleOpenDynamicSelect} + isLoading={isLoading} /> ) : ( @@ -412,6 +460,7 @@ const VarReferencePicker: FC = ({ )}
{!hasValue && } + {isLoading && } {isEnv && } {isChatVar && }
= ({ {!isValidVar && } ) - :
{placeholder ?? t('workflow.common.setVarValuePlaceholder')}
} + :
+ {isLoading ? ( +
+ + {placeholder ?? t('workflow.common.setVarValuePlaceholder')} +
+ ) : ( + placeholder ?? t('workflow.common.setVarValuePlaceholder') + )} +
}
diff --git a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx index 1a609c58f5..b56c5c1318 100644 --- a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx +++ b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx @@ -58,6 +58,8 @@ const InputVarList: FC = ({ return 'ModelSelector' else if (type === FormTypeEnum.toolSelector) return 'ToolSelector' + else if (type === FormTypeEnum.dynamicSelect) + return 'DynamicSelect' else return 'String' } @@ -149,6 +151,7 @@ const InputVarList: FC = ({ const handleOpen = useCallback((index: number) => { return () => onOpen(index) }, [onOpen]) + return (
{ @@ -163,7 +166,8 @@ const InputVarList: FC = ({ } = schema const varInput = value[variable] const isNumber = type === FormTypeEnum.textNumber - const isSelect = type === FormTypeEnum.select + const isDynamicSelect = type === FormTypeEnum.dynamicSelect + const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files const isAppSelector = type === FormTypeEnum.appSelector const isModelSelector = type === FormTypeEnum.modelSelector @@ -198,7 +202,7 @@ const InputVarList: FC = ({ value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])} onChange={handleNotMixedTypeChange(variable)} onOpen={handleOpen(index)} - defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)} + defaultVarKindType={varInput?.type || ((isNumber || isDynamicSelect) ? VarKindType.constant : VarKindType.variable)} isSupportConstantValue={isSupportConstantValue} filterVar={isNumber ? filterVar : undefined} availableVars={isSelect ? availableVars : undefined}