| from core.plugin.impl.exc import PluginDaemonClientSideError | from core.plugin.impl.exc import PluginDaemonClientSideError | ||||
| from libs.login import login_required | from libs.login import login_required | ||||
| from models.account import TenantPluginPermission | from models.account import TenantPluginPermission | ||||
| from services.plugin.plugin_parameter_service import PluginParameterService | |||||
| from services.plugin.plugin_permission_service import PluginPermissionService | from services.plugin.plugin_permission_service import PluginPermissionService | ||||
| from services.plugin.plugin_service import PluginService | from services.plugin.plugin_service import PluginService | ||||
| ) | ) | ||||
| class PluginFetchDynamicSelectOptionsApi(Resource): | |||||
| @setup_required | |||||
| @login_required | |||||
| @account_initialization_required | |||||
| def get(self): | |||||
| # check if the user is admin or owner | |||||
| if not current_user.is_admin_or_owner: | |||||
| raise Forbidden() | |||||
| tenant_id = current_user.current_tenant_id | |||||
| user_id = current_user.id | |||||
| parser = reqparse.RequestParser() | |||||
| parser.add_argument("plugin_id", type=str, required=True, location="args") | |||||
| parser.add_argument("provider", type=str, required=True, location="args") | |||||
| parser.add_argument("action", type=str, required=True, location="args") | |||||
| parser.add_argument("parameter", type=str, required=True, location="args") | |||||
| parser.add_argument("provider_type", type=str, required=True, location="args") | |||||
| args = parser.parse_args() | |||||
| try: | |||||
| options = PluginParameterService.get_dynamic_select_options( | |||||
| tenant_id, | |||||
| user_id, | |||||
| args["plugin_id"], | |||||
| args["provider"], | |||||
| args["action"], | |||||
| args["parameter"], | |||||
| args["provider_type"], | |||||
| ) | |||||
| except PluginDaemonClientSideError as e: | |||||
| raise ValueError(e) | |||||
| return jsonable_encoder({"options": options}) | |||||
| api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") | api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") | ||||
| api.add_resource(PluginListApi, "/workspaces/current/plugin/list") | api.add_resource(PluginListApi, "/workspaces/current/plugin/list") | ||||
| api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") | api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") | ||||
| api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change") | api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change") | ||||
| api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") | api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") | ||||
| api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options") |
| 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" | ||||
| 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 |
| from collections.abc import Mapping | |||||
| from collections.abc import Mapping, Sequence | |||||
| from datetime import datetime | from datetime import datetime | ||||
| from enum import StrEnum | from enum import StrEnum | ||||
| from typing import Any, Generic, Optional, TypeVar | from typing import Any, Generic, Optional, TypeVar | ||||
| from core.model_runtime.entities.model_entities import AIModelEntity | from core.model_runtime.entities.model_entities import AIModelEntity | ||||
| from core.model_runtime.entities.provider_entities import ProviderEntity | from core.model_runtime.entities.provider_entities import ProviderEntity | ||||
| from core.plugin.entities.base import BasePluginEntity | from core.plugin.entities.base import BasePluginEntity | ||||
| from core.plugin.entities.parameters import PluginParameterOption | |||||
| from core.plugin.entities.plugin import PluginDeclaration, PluginEntity | from core.plugin.entities.plugin import PluginDeclaration, PluginEntity | ||||
| from core.tools.entities.common_entities import I18nObject | from core.tools.entities.common_entities import I18nObject | ||||
| from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin | from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin | ||||
| class PluginListResponse(BaseModel): | class PluginListResponse(BaseModel): | ||||
| list: list[PluginEntity] | list: list[PluginEntity] | ||||
| total: int | total: int | ||||
| class PluginDynamicSelectOptionsResponse(BaseModel): | |||||
| options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.") |
| from collections.abc import Mapping | |||||
| from typing import Any | |||||
| from core.plugin.entities.plugin import GenericProviderID | |||||
| from core.plugin.entities.plugin_daemon import PluginDynamicSelectOptionsResponse | |||||
| from core.plugin.impl.base import BasePluginClient | |||||
| class DynamicSelectClient(BasePluginClient): | |||||
| def fetch_dynamic_select_options( | |||||
| self, | |||||
| tenant_id: str, | |||||
| user_id: str, | |||||
| plugin_id: str, | |||||
| provider: str, | |||||
| action: str, | |||||
| credentials: Mapping[str, Any], | |||||
| parameter: str, | |||||
| ) -> PluginDynamicSelectOptionsResponse: | |||||
| """ | |||||
| Fetch dynamic select options for a plugin parameter. | |||||
| """ | |||||
| response = self._request_with_plugin_daemon_response_stream( | |||||
| "POST", | |||||
| f"plugin/{tenant_id}/dispatch/dynamic_select/fetch_parameter_options", | |||||
| PluginDynamicSelectOptionsResponse, | |||||
| data={ | |||||
| "user_id": user_id, | |||||
| "data": { | |||||
| "provider": GenericProviderID(provider).provider_name, | |||||
| "credentials": credentials, | |||||
| "provider_action": action, | |||||
| "parameter": parameter, | |||||
| }, | |||||
| }, | |||||
| headers={ | |||||
| "X-Plugin-ID": plugin_id, | |||||
| "Content-Type": "application/json", | |||||
| }, | |||||
| ) | |||||
| for options in response: | |||||
| return options | |||||
| raise ValueError("Plugin service returned no options") |
| 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 |
| cached_credentials = cache.get() | cached_credentials = cache.get() | ||||
| if cached_credentials: | if cached_credentials: | ||||
| return cached_credentials | return cached_credentials | ||||
| data = self._deep_copy(data) | data = self._deep_copy(data) | ||||
| # get fields need to be decrypted | # get fields need to be decrypted | ||||
| fields = dict[str, BasicProviderConfig]() | fields = dict[str, BasicProviderConfig]() |
| from collections.abc import Mapping, Sequence | |||||
| from typing import Any, Literal | |||||
| from sqlalchemy.orm import Session | |||||
| from core.plugin.entities.parameters import PluginParameterOption | |||||
| from core.plugin.impl.dynamic_select import DynamicSelectClient | |||||
| from core.tools.tool_manager import ToolManager | |||||
| from core.tools.utils.configuration import ProviderConfigEncrypter | |||||
| from extensions.ext_database import db | |||||
| from models.tools import BuiltinToolProvider | |||||
| class PluginParameterService: | |||||
| @staticmethod | |||||
| def get_dynamic_select_options( | |||||
| tenant_id: str, | |||||
| user_id: str, | |||||
| plugin_id: str, | |||||
| provider: str, | |||||
| action: str, | |||||
| parameter: str, | |||||
| provider_type: Literal["tool"], | |||||
| ) -> Sequence[PluginParameterOption]: | |||||
| """ | |||||
| Get dynamic select options for a plugin parameter. | |||||
| Args: | |||||
| tenant_id: The tenant ID. | |||||
| plugin_id: The plugin ID. | |||||
| provider: The provider name. | |||||
| action: The action name. | |||||
| parameter: The parameter name. | |||||
| """ | |||||
| credentials: Mapping[str, Any] = {} | |||||
| match provider_type: | |||||
| case "tool": | |||||
| provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) | |||||
| # init tool configuration | |||||
| tool_configuration = ProviderConfigEncrypter( | |||||
| tenant_id=tenant_id, | |||||
| config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], | |||||
| provider_type=provider_controller.provider_type.value, | |||||
| provider_identity=provider_controller.entity.identity.name, | |||||
| ) | |||||
| # check if credentials are required | |||||
| if not provider_controller.need_credentials: | |||||
| credentials = {} | |||||
| else: | |||||
| # fetch credentials from db | |||||
| with Session(db.engine) as session: | |||||
| db_record = ( | |||||
| session.query(BuiltinToolProvider) | |||||
| .filter( | |||||
| BuiltinToolProvider.tenant_id == tenant_id, | |||||
| BuiltinToolProvider.provider == provider, | |||||
| ) | |||||
| .first() | |||||
| ) | |||||
| if db_record is None: | |||||
| raise ValueError(f"Builtin provider {provider} not found when fetching credentials") | |||||
| credentials = tool_configuration.decrypt(db_record.credentials) | |||||
| case _: | |||||
| raise ValueError(f"Invalid provider type: {provider_type}") | |||||
| return ( | |||||
| DynamicSelectClient() | |||||
| .fetch_dynamic_select_options(tenant_id, user_id, plugin_id, provider, action, credentials, parameter) | |||||
| .options | |||||
| ) |
| '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 { | ||||
| 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, | ||||
| 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) | ||||
| // 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) { | ||||
| <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) => { | ||||
| </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 |
| 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 { |
| 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 | ||||
| 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 | ||||
| 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' | ||||
| 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 && ( |
| RiArrowDownSLine, | RiArrowDownSLine, | ||||
| RiCloseLine, | RiCloseLine, | ||||
| RiErrorWarningFill, | RiErrorWarningFill, | ||||
| RiLoader4Line, | |||||
| RiMoreLine, | RiMoreLine, | ||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import produce from 'immer' | import produce from 'immer' | ||||
| import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils' | import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils' | ||||
| 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 { 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 { 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' | ||||
| import { isExceptionVariable } from '@/app/components/workflow/utils' | import { isExceptionVariable } from '@/app/components/workflow/utils' | ||||
| import VarFullPathPanel from './var-full-path-panel' | import VarFullPathPanel from './var-full-path-panel' | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| import { useFetchDynamicOptions } from '@/service/use-plugins' | |||||
| import type { Tool } from '@/app/components/tools/types' | |||||
| const TRIGGER_DEFAULT_WIDTH = 227 | const TRIGGER_DEFAULT_WIDTH = 227 | ||||
| minWidth?: number | minWidth?: number | ||||
| popupFor?: 'assigned' | 'toAssigned' | popupFor?: 'assigned' | 'toAssigned' | ||||
| zIndex?: number | zIndex?: number | ||||
| currentTool?: Tool | |||||
| currentProvider?: ToolWithProvider | |||||
| } | } | ||||
| const DEFAULT_VALUE_SELECTOR: Props['value'] = [] | const DEFAULT_VALUE_SELECTOR: Props['value'] = [] | ||||
| minWidth, | minWidth, | ||||
| popupFor, | popupFor, | ||||
| zIndex, | zIndex, | ||||
| currentTool, | |||||
| currentProvider, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const store = useStoreApi() | const store = useStoreApi() | ||||
| 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 { 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 ( | return ( | ||||
| <div className={cn(className, !readonly && 'cursor-pointer')}> | <div className={cn(className, !readonly && 'cursor-pointer')}> | ||||
| <PortalToFollowElem | <PortalToFollowElem | ||||
| <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} | ||||
| isLoading={isLoading} | |||||
| /> | /> | ||||
| ) | ) | ||||
| : ( | : ( | ||||
| )} | )} | ||||
| <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={{ | ||||
| {!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> |
| import type { ToolVarInputs } from '../types' | import type { ToolVarInputs } from '../types' | ||||
| import { VarType as VarKindType } from '../types' | import { VarType as VarKindType } from '../types' | ||||
| import cn from '@/utils/classnames' | 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 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 { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | ||||
| import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' | import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' | ||||
| import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' | import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' | ||||
| import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' | import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| import type { Tool } from '@/app/components/tools/types' | |||||
| type Props = { | type Props = { | ||||
| readOnly: boolean | readOnly: boolean | ||||
| onOpen?: (index: number) => void | onOpen?: (index: number) => void | ||||
| isSupportConstantValue?: boolean | isSupportConstantValue?: boolean | ||||
| filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean | filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean | ||||
| currentTool?: Tool | |||||
| currentProvider?: ToolWithProvider | |||||
| } | } | ||||
| const InputVarList: FC<Props> = ({ | const InputVarList: FC<Props> = ({ | ||||
| onOpen = noop, | onOpen = noop, | ||||
| isSupportConstantValue, | isSupportConstantValue, | ||||
| filterVar, | filterVar, | ||||
| currentTool, | |||||
| currentProvider, | |||||
| }) => { | }) => { | ||||
| const language = useLanguage() | const language = useLanguage() | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| return 'ModelSelector' | return 'ModelSelector' | ||||
| else if (type === FormTypeEnum.toolSelector) | else if (type === FormTypeEnum.toolSelector) | ||||
| return 'ToolSelector' | return 'ToolSelector' | ||||
| else if (type === FormTypeEnum.dynamicSelect || type === FormTypeEnum.select) | |||||
| return 'Select' | |||||
| else | else | ||||
| return 'String' | return 'String' | ||||
| } | } | ||||
| 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'> | ||||
| { | { | ||||
| } = 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 | ||||
| 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} | ||||
| schema={schema} | schema={schema} | ||||
| currentTool={currentTool} | |||||
| currentProvider={currentProvider} | |||||
| /> | /> | ||||
| )} | )} | ||||
| {isFile && ( | {isFile && ( |
| isLoading, | isLoading, | ||||
| outputSchema, | outputSchema, | ||||
| hasObjectOutput, | hasObjectOutput, | ||||
| currTool, | |||||
| } = useConfig(id, data) | } = useConfig(id, data) | ||||
| if (isLoading) { | if (isLoading) { | ||||
| filterVar={filterVar} | filterVar={filterVar} | ||||
| isSupportConstantValue | isSupportConstantValue | ||||
| onOpen={handleOnVarOpen} | onOpen={handleOnVarOpen} | ||||
| currentProvider={currCollection} | |||||
| currentTool={currTool} | |||||
| /> | /> | ||||
| </Field> | </Field> | ||||
| )} | )} |
| import { useCallback, useEffect } from 'react' | import { useCallback, useEffect } from 'react' | ||||
| import type { | import type { | ||||
| FormOption, | |||||
| ModelProvider, | ModelProvider, | ||||
| } from '@/app/components/header/account-setting/model-provider-page/declarations' | } from '@/app/components/header/account-setting/model-provider-page/declarations' | ||||
| import { fetchModelProviderModelList } from '@/service/common' | import { fetchModelProviderModelList } from '@/service/common' | ||||
| refreshPluginList(category ? { category } as any : undefined, !category) | refreshPluginList(category ? { category } as any : undefined, !category) | ||||
| } | } | ||||
| } | } | ||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||||
| }, [isRefetching]) | }, [isRefetching]) | ||||
| const handleRefetch = useCallback(() => { | const handleRefetch = useCallback(() => { | ||||
| enabled: !!providerName, | 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, | |||||
| }, | |||||
| }), | |||||
| }) | |||||
| } |