feat: add DYNAMIC_SELECT parameter type for dynamic options in parameter entities (#21425)
This commit is contained in:
@@ -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<ISelectProps> = ({
|
||||
className,
|
||||
@@ -178,17 +180,20 @@ const SimpleSelect: FC<ISelectProps> = ({
|
||||
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<Item | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let defaultSelect = null
|
||||
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
|
||||
}, [defaultValue])
|
||||
|
||||
const listboxRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<Listbox
|
||||
<Listbox ref={listboxRef}
|
||||
value={selectedItem}
|
||||
onChange={(value: Item) => {
|
||||
if (!disabled) {
|
||||
@@ -212,10 +219,17 @@ const SimpleSelect: FC<ISelectProps> = ({
|
||||
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
|
||||
{renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>}
|
||||
{!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="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
|
||||
onClick={(e) => {
|
||||
@@ -237,7 +251,7 @@ const SimpleSelect: FC<ISelectProps> = ({
|
||||
</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)}>
|
||||
{items.map((item: Item) => (
|
||||
<ListboxOption
|
||||
|
@@ -19,12 +19,14 @@ export enum FormTypeEnum {
|
||||
toolSelector = 'tool-selector',
|
||||
multiToolSelector = 'array[tools]',
|
||||
appSelector = 'app-selector',
|
||||
dynamicSelect = 'dynamic-select',
|
||||
}
|
||||
|
||||
export type FormOption = {
|
||||
label: TypeWithI18N
|
||||
value: string
|
||||
show_on: FormShowOnObject[]
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export enum ModelTypeEnum {
|
||||
|
@@ -13,6 +13,8 @@ type Props = {
|
||||
readonly: boolean
|
||||
value: string
|
||||
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
|
||||
onOpenChange?: (open: boolean) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_SCHEMA = {} as CredentialFormSchema
|
||||
@@ -22,6 +24,8 @@ const ConstantField: FC<Props> = ({
|
||||
readonly,
|
||||
value,
|
||||
onChange,
|
||||
onOpenChange,
|
||||
isLoading,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const placeholder = (schema as CredentialFormSchemaSelect).placeholder
|
||||
@@ -36,7 +40,7 @@ const ConstantField: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{schema.type === FormTypeEnum.select && (
|
||||
{(schema.type === FormTypeEnum.select || schema.type === FormTypeEnum.dynamicSelect) && (
|
||||
<SimpleSelect
|
||||
wrapperClassName='w-full !h-8'
|
||||
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 }))}
|
||||
onSelect={item => handleSelectChange(item.value)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
onOpenChange={onOpenChange}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
{schema.type === FormTypeEnum.textNumber && (
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
RiArrowDownSLine,
|
||||
RiCloseLine,
|
||||
RiErrorWarningFill,
|
||||
RiLoader4Line,
|
||||
RiMoreLine,
|
||||
} from '@remixicon/react'
|
||||
import produce from 'immer'
|
||||
@@ -16,8 +17,9 @@ import VarReferencePopup from './var-reference-popup'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils'
|
||||
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 { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { CredentialFormSchemaSelect } 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'
|
||||
@@ -40,6 +42,8 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import { isExceptionVariable } from '@/app/components/workflow/utils'
|
||||
import VarFullPathPanel from './var-full-path-panel'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
const TRIGGER_DEFAULT_WIDTH = 227
|
||||
|
||||
@@ -68,6 +72,8 @@ type Props = {
|
||||
minWidth?: number
|
||||
popupFor?: 'assigned' | 'toAssigned'
|
||||
zIndex?: number
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
}
|
||||
|
||||
const DEFAULT_VALUE_SELECTOR: Props['value'] = []
|
||||
@@ -97,6 +103,8 @@ const VarReferencePicker: FC<Props> = ({
|
||||
minWidth,
|
||||
popupFor,
|
||||
zIndex,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
@@ -316,6 +324,42 @@ const VarReferencePicker: FC<Props> = ({
|
||||
|
||||
return null
|
||||
}, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type])
|
||||
|
||||
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
|
||||
currentProvider?.plugin_id || '', currentProvider?.name || '', currentTool?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
|
||||
'tool',
|
||||
)
|
||||
const handleFetchDynamicOptions = async () => {
|
||||
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider)
|
||||
return
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const data = await fetchDynamicOptions()
|
||||
setDynamicOptions(data?.options || [])
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
handleFetchDynamicOptions()
|
||||
}, [currentTool, currentProvider, schema])
|
||||
|
||||
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 (
|
||||
<div className={cn(className, !readonly && 'cursor-pointer')}>
|
||||
<PortalToFollowElem
|
||||
@@ -366,8 +410,9 @@ const VarReferencePicker: FC<Props> = ({
|
||||
<ConstantField
|
||||
value={value as string}
|
||||
onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)}
|
||||
schema={schema as CredentialFormSchema}
|
||||
schema={schemaWithDynamicSelect as CredentialFormSchema}
|
||||
readonly={readonly}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
@@ -412,6 +457,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
)}
|
||||
<div className='flex items-center text-text-accent'>
|
||||
{!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' />}
|
||||
{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={{
|
||||
@@ -424,7 +470,16 @@ const VarReferencePicker: FC<Props> = ({
|
||||
{!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>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { ToolVarInputs } from '../types'
|
||||
import { VarType as VarKindType } from '../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
@@ -17,6 +17,7 @@ import { VarType } from '@/app/components/workflow/types'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
import { noop } from 'lodash-es'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
@@ -27,6 +28,8 @@ type Props = {
|
||||
onOpen?: (index: number) => void
|
||||
isSupportConstantValue?: boolean
|
||||
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
}
|
||||
|
||||
const InputVarList: FC<Props> = ({
|
||||
@@ -38,6 +41,8 @@ const InputVarList: FC<Props> = ({
|
||||
onOpen = noop,
|
||||
isSupportConstantValue,
|
||||
filterVar,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const { t } = useTranslation()
|
||||
@@ -58,6 +63,8 @@ const InputVarList: FC<Props> = ({
|
||||
return 'ModelSelector'
|
||||
else if (type === FormTypeEnum.toolSelector)
|
||||
return 'ToolSelector'
|
||||
else if (type === FormTypeEnum.dynamicSelect || type === FormTypeEnum.select)
|
||||
return 'Select'
|
||||
else
|
||||
return 'String'
|
||||
}
|
||||
@@ -149,6 +156,7 @@ const InputVarList: FC<Props> = ({
|
||||
const handleOpen = useCallback((index: number) => {
|
||||
return () => onOpen(index)
|
||||
}, [onOpen])
|
||||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
{
|
||||
@@ -163,7 +171,8 @@ const InputVarList: FC<Props> = ({
|
||||
} = 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,11 +207,13 @@ const InputVarList: FC<Props> = ({
|
||||
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}
|
||||
schema={schema}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
)}
|
||||
{isFile && (
|
||||
|
@@ -42,6 +42,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
||||
isLoading,
|
||||
outputSchema,
|
||||
hasObjectOutput,
|
||||
currTool,
|
||||
} = useConfig(id, data)
|
||||
|
||||
if (isLoading) {
|
||||
@@ -80,6 +81,8 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
||||
filterVar={filterVar}
|
||||
isSupportConstantValue
|
||||
onOpen={handleOnVarOpen}
|
||||
currentProvider={currCollection}
|
||||
currentTool={currTool}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import type {
|
||||
FormOption,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { fetchModelProviderModelList } from '@/service/common'
|
||||
@@ -477,7 +478,7 @@ export const usePluginTaskList = (category?: PluginType) => {
|
||||
refreshPluginList(category ? { category } as any : undefined, !category)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isRefetching])
|
||||
|
||||
const handleRefetch = useCallback(() => {
|
||||
@@ -571,3 +572,17 @@ export const usePluginInfo = (providerName?: string) => {
|
||||
enabled: !!providerName,
|
||||
})
|
||||
}
|
||||
|
||||
export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type: 'tool') => {
|
||||
return useMutation({
|
||||
mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
|
||||
params: {
|
||||
plugin_id,
|
||||
provider,
|
||||
action,
|
||||
parameter,
|
||||
provider_type,
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user