feat: add DYNAMIC_SELECT parameter type for dynamic options in parameter entities

pull/21425/head
Yeuoly 11 months ago
parent 45146edb31
commit e9887adcf6

@ -15,6 +15,11 @@ class CommonParameterType(StrEnum):
MODEL_SELECTOR = "model-selector" MODEL_SELECTOR = "model-selector"
TOOLS_SELECTOR = "array[tools]" 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" # TOOL_SELECTOR = "tool-selector"

@ -35,6 +35,7 @@ class PluginParameterType(enum.StrEnum):
APP_SELECTOR = CommonParameterType.APP_SELECTOR.value APP_SELECTOR = CommonParameterType.APP_SELECTOR.value
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value
DYNAMIC_SELECT = CommonParameterType.DYNAMIC_SELECT.value
# deprecated, should not use. # deprecated, should not use.
SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value

@ -240,6 +240,7 @@ class ToolParameter(PluginParameter):
FILES = PluginParameterType.FILES.value FILES = PluginParameterType.FILES.value
APP_SELECTOR = PluginParameterType.APP_SELECTOR.value APP_SELECTOR = PluginParameterType.APP_SELECTOR.value
MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value
DYNAMIC_SELECT = PluginParameterType.DYNAMIC_SELECT.value
# deprecated, should not use. # deprecated, should not use.
SYSTEM_FILES = PluginParameterType.SYSTEM_FILES.value SYSTEM_FILES = PluginParameterType.SYSTEM_FILES.value

@ -1,10 +1,10 @@
'use client' 'use client'
import type { FC } from 'react' 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 { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid' import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid'
import Badge from '../badge/index' import Badge from '../badge/index'
import { RiCheckLine } from '@remixicon/react' import { RiCheckLine, RiLoader4Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { import {
@ -51,6 +51,8 @@ export type ISelectProps = {
item: Item item: Item
selected: boolean selected: boolean
}) => React.ReactNode }) => React.ReactNode
isLoading?: boolean
onOpenChange?: (open: boolean) => void
} }
const Select: FC<ISelectProps> = ({ const Select: FC<ISelectProps> = ({
className, className,
@ -178,17 +180,20 @@ const SimpleSelect: FC<ISelectProps> = ({
defaultValue = 1, defaultValue = 1,
disabled = false, disabled = false,
onSelect, onSelect,
onOpenChange,
placeholder, placeholder,
optionWrapClassName, optionWrapClassName,
optionClassName, optionClassName,
hideChecked, hideChecked,
notClearable, notClearable,
renderOption, renderOption,
isLoading = false,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const localPlaceholder = placeholder || t('common.placeholder.select') const localPlaceholder = placeholder || t('common.placeholder.select')
const [selectedItem, setSelectedItem] = useState<Item | null>(null) const [selectedItem, setSelectedItem] = useState<Item | null>(null)
useEffect(() => { useEffect(() => {
let defaultSelect = null let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue) const existed = items.find((item: Item) => item.value === defaultValue)
@ -199,8 +204,10 @@ const SimpleSelect: FC<ISelectProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultValue]) }, [defaultValue])
const listboxRef = useRef<HTMLDivElement>(null)
return ( return (
<Listbox <Listbox ref={listboxRef}
value={selectedItem} value={selectedItem}
onChange={(value: Item) => { onChange={(value: Item) => {
if (!disabled) { if (!disabled) {
@ -212,10 +219,17 @@ const SimpleSelect: FC<ISelectProps> = ({
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}> <div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
{renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>} {renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>}
{!renderTrigger && ( {!renderTrigger && (
<ListboxButton 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)}> <ListboxButton onClick={() => {
// 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)}>
<span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span> <span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2"> <span className="absolute inset-y-0 right-0 flex items-center pr-2">
{(selectedItem && !notClearable) {isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />
: (selectedItem && !notClearable)
? ( ? (
<XMarkIcon <XMarkIcon
onClick={(e) => { onClick={(e) => {
@ -237,7 +251,7 @@ const SimpleSelect: FC<ISelectProps> = ({
</ListboxButton> </ListboxButton>
)} )}
{!disabled && ( {(!disabled) && (
<ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-xl bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}> <ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-xl bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => ( {items.map((item: Item) => (
<ListboxOption <ListboxOption

@ -19,12 +19,14 @@ export enum FormTypeEnum {
toolSelector = 'tool-selector', toolSelector = 'tool-selector',
multiToolSelector = 'array[tools]', multiToolSelector = 'array[tools]',
appSelector = 'app-selector', appSelector = 'app-selector',
dynamicSelect = 'dynamic-select',
} }
export type FormOption = { export type FormOption = {
label: TypeWithI18N label: TypeWithI18N
value: string value: string
show_on: FormShowOnObject[] show_on: FormShowOnObject[]
icon?: string
} }
export enum ModelTypeEnum { export enum ModelTypeEnum {

@ -13,6 +13,8 @@ type Props = {
readonly: boolean readonly: boolean
value: string value: string
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
onOpenChange?: (open: boolean) => void
isLoading?: boolean
} }
const DEFAULT_SCHEMA = {} as CredentialFormSchema const DEFAULT_SCHEMA = {} as CredentialFormSchema
@ -22,6 +24,8 @@ const ConstantField: FC<Props> = ({
readonly, readonly,
value, value,
onChange, onChange,
onOpenChange,
isLoading,
}) => { }) => {
const language = useLanguage() const language = useLanguage()
const placeholder = (schema as CredentialFormSchemaSelect).placeholder const placeholder = (schema as CredentialFormSchemaSelect).placeholder
@ -36,7 +40,7 @@ const ConstantField: FC<Props> = ({
return ( return (
<> <>
{schema.type === FormTypeEnum.select && ( {(schema.type === FormTypeEnum.select || schema.type === FormTypeEnum.dynamicSelect) && (
<SimpleSelect <SimpleSelect
wrapperClassName='w-full !h-8' wrapperClassName='w-full !h-8'
className='flex items-center' className='flex items-center'
@ -45,6 +49,8 @@ const ConstantField: FC<Props> = ({
items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))} items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))}
onSelect={item => handleSelectChange(item.value)} onSelect={item => handleSelectChange(item.value)}
placeholder={placeholder?.[language] || placeholder?.en_US} placeholder={placeholder?.[language] || placeholder?.en_US}
onOpenChange={onOpenChange}
isLoading={isLoading}
/> />
)} )}
{schema.type === FormTypeEnum.textNumber && ( {schema.type === FormTypeEnum.textNumber && (

@ -6,6 +6,7 @@ import {
RiArrowDownSLine, RiArrowDownSLine,
RiCloseLine, RiCloseLine,
RiErrorWarningFill, RiErrorWarningFill,
RiLoader4Line,
RiMoreLine, RiMoreLine,
} from '@remixicon/react' } from '@remixicon/react'
import produce from 'immer' import produce from 'immer'
@ -17,7 +18,7 @@ import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStruct
import ConstantField from './constant-field' import ConstantField from './constant-field'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' 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 { BlockEnum } from '@/app/components/workflow/types'
import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common' import { Line3 } from '@/app/components/base/icons/src/public/common'
@ -68,6 +69,7 @@ type Props = {
minWidth?: number minWidth?: number
popupFor?: 'assigned' | 'toAssigned' popupFor?: 'assigned' | 'toAssigned'
zIndex?: number zIndex?: number
isLoading?: boolean
} }
const DEFAULT_VALUE_SELECTOR: Props['value'] = [] const DEFAULT_VALUE_SELECTOR: Props['value'] = []
@ -316,6 +318,50 @@ const VarReferencePicker: FC<Props> = ({
return null return null
}, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type]) }, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type])
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(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 ( return (
<div className={cn(className, !readonly && 'cursor-pointer')}> <div className={cn(className, !readonly && 'cursor-pointer')}>
<PortalToFollowElem <PortalToFollowElem
@ -366,8 +412,10 @@ const VarReferencePicker: FC<Props> = ({
<ConstantField <ConstantField
value={value as string} value={value as string}
onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)} onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)}
schema={schema as CredentialFormSchema} schema={schemaWithDynamicSelect as CredentialFormSchema}
readonly={readonly} readonly={readonly}
onOpenChange={handleOpenDynamicSelect}
isLoading={isLoading}
/> />
) )
: ( : (
@ -412,6 +460,7 @@ const VarReferencePicker: FC<Props> = ({
)} )}
<div className='flex items-center text-text-accent'> <div className='flex items-center text-text-accent'>
{!hasValue && <Variable02 className='h-3.5 w-3.5' />} {!hasValue && <Variable02 className='h-3.5 w-3.5' />}
{isLoading && <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />}
{isEnv && <Env className='h-3.5 w-3.5 text-util-colors-violet-violet-600' />} {isEnv && <Env className='h-3.5 w-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />} {isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{ <div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{
@ -424,7 +473,16 @@ const VarReferencePicker: FC<Props> = ({
{!isValidVar && <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' />} {!isValidVar && <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' />}
</> </>
) )
: <div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>{placeholder ?? t('workflow.common.setVarValuePlaceholder')}</div>} : <div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>
{isLoading ? (
<div className='flex items-center'>
<RiLoader4Line className='mr-1 h-3.5 w-3.5 animate-spin text-text-secondary' />
<span>{placeholder ?? t('workflow.common.setVarValuePlaceholder')}</span>
</div>
) : (
placeholder ?? t('workflow.common.setVarValuePlaceholder')
)}
</div>}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>

@ -58,6 +58,8 @@ const InputVarList: FC<Props> = ({
return 'ModelSelector' return 'ModelSelector'
else if (type === FormTypeEnum.toolSelector) else if (type === FormTypeEnum.toolSelector)
return 'ToolSelector' return 'ToolSelector'
else if (type === FormTypeEnum.dynamicSelect)
return 'DynamicSelect'
else else
return 'String' return 'String'
} }
@ -149,6 +151,7 @@ const InputVarList: FC<Props> = ({
const handleOpen = useCallback((index: number) => { const handleOpen = useCallback((index: number) => {
return () => onOpen(index) return () => onOpen(index)
}, [onOpen]) }, [onOpen])
return ( return (
<div className='space-y-3'> <div className='space-y-3'>
{ {
@ -163,7 +166,8 @@ const InputVarList: FC<Props> = ({
} = schema } = schema
const varInput = value[variable] const varInput = value[variable]
const isNumber = type === FormTypeEnum.textNumber 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 isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isAppSelector = type === FormTypeEnum.appSelector const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector const isModelSelector = type === FormTypeEnum.modelSelector
@ -198,7 +202,7 @@ const InputVarList: FC<Props> = ({
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])} value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)} onChange={handleNotMixedTypeChange(variable)}
onOpen={handleOpen(index)} onOpen={handleOpen(index)}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)} defaultVarKindType={varInput?.type || ((isNumber || isDynamicSelect) ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue={isSupportConstantValue} isSupportConstantValue={isSupportConstantValue}
filterVar={isNumber ? filterVar : undefined} filterVar={isNumber ? filterVar : undefined}
availableVars={isSelect ? availableVars : undefined} availableVars={isSelect ? availableVars : undefined}

Loading…
Cancel
Save