Merge branch 'tp' into dev/plugin-deploy

This commit is contained in:
JzoNg 2025-02-08 22:12:34 +08:00
commit 82ead2735b
26 changed files with 672 additions and 129 deletions

View File

@ -5,66 +5,73 @@ import classNames from '@/utils/classnames'
type SwitchProps = {
onChange?: (value: boolean) => void
size?: 'sm' | 'md' | 'lg' | 'l'
size?: 'xs' | 'sm' | 'md' | 'lg' | 'l'
defaultValue?: boolean
disabled?: boolean
className?: string
}
const Switch = React.forwardRef<HTMLButtonElement>(({ onChange, size = 'md', defaultValue = false, disabled = false, className }: SwitchProps, ref) => {
const [enabled, setEnabled] = useState(defaultValue)
useEffect(() => {
setEnabled(defaultValue)
}, [defaultValue])
const wrapStyle = {
lg: 'h-6 w-11',
l: 'h-5 w-9',
md: 'h-4 w-7',
sm: 'h-3 w-5',
}
const Switch = React.forwardRef(
({ onChange, size = 'md', defaultValue = false, disabled = false, className }: SwitchProps,
propRef: React.Ref<HTMLButtonElement>) => {
const [enabled, setEnabled] = useState(defaultValue)
useEffect(() => {
setEnabled(defaultValue)
}, [defaultValue])
const wrapStyle = {
lg: 'h-6 w-11',
l: 'h-5 w-9',
md: 'h-4 w-7',
sm: 'h-3 w-5',
xs: 'h-2.5 w-3.5',
}
const circleStyle = {
lg: 'h-5 w-5',
l: 'h-4 w-4',
md: 'h-3 w-3',
sm: 'h-2 w-2',
}
const circleStyle = {
lg: 'h-5 w-5',
l: 'h-4 w-4',
md: 'h-3 w-3',
sm: 'h-2 w-2',
xs: 'h-1.5 w-1',
}
const translateLeft = {
lg: 'translate-x-5',
l: 'translate-x-4',
md: 'translate-x-3',
sm: 'translate-x-2',
}
return (
<OriginalSwitch
ref={ref}
checked={enabled}
onChange={(checked: boolean) => {
if (disabled)
return
setEnabled(checked)
onChange?.(checked)
}}
className={classNames(
wrapStyle[size],
enabled ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked',
'relative inline-flex flex-shrink-0 cursor-pointer rounded-[5px] border-2 border-transparent transition-colors duration-200 ease-in-out',
disabled ? '!opacity-50 !cursor-not-allowed' : '',
className,
)}
>
<span
aria-hidden="true"
const translateLeft = {
lg: 'translate-x-5',
l: 'translate-x-4',
md: 'translate-x-3',
sm: 'translate-x-2',
xs: 'translate-x-1.5',
}
return (
<OriginalSwitch
ref={propRef}
checked={enabled}
onChange={(checked: boolean) => {
if (disabled)
return
setEnabled(checked)
onChange?.(checked)
}}
className={classNames(
circleStyle[size],
enabled ? translateLeft[size] : 'translate-x-0',
'pointer-events-none inline-block transform rounded-[3px] bg-components-toggle-knob shadow ring-0 transition duration-200 ease-in-out',
wrapStyle[size],
enabled ? 'bg-components-toggle-bg' : 'bg-components-toggle-bg-unchecked',
'relative inline-flex flex-shrink-0 cursor-pointer rounded-[5px] border-2 border-transparent transition-colors duration-200 ease-in-out',
disabled ? '!opacity-50 !cursor-not-allowed' : '',
size === 'xs' && 'rounded-sm',
className,
)}
/>
</OriginalSwitch>
)
})
>
<span
aria-hidden="true"
className={classNames(
circleStyle[size],
enabled ? translateLeft[size] : 'translate-x-0',
size === 'xs' && 'rounded-[1px]',
'pointer-events-none inline-block transform rounded-[3px] bg-components-toggle-knob shadow ring-0 transition duration-200 ease-in-out',
)}
/>
</OriginalSwitch>
)
})
Switch.displayName = 'Switch'

View File

@ -3,47 +3,51 @@ import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
interface Option {
type Option = {
value: string
text: string | JSX.Element
}
interface ItemProps {
type ItemProps = {
className?: string
isActive: boolean
onClick: (v: string) => void
option: Option
smallItem?: boolean
}
const Item: FC<ItemProps> = ({
className,
isActive,
onClick,
option,
smallItem,
}) => {
return (
<div
key={option.value}
className={cn(
'relative pb-2.5 system-xl-semibold',
'relative pb-2.5 ',
!isActive && 'cursor-pointer',
smallItem ? 'system-sm-semibold-uppercase' : 'system-xl-semibold',
className,
)}
onClick={() => !isActive && onClick(option.value)}
>
<div className={cn(isActive ? 'text-text-primary' : 'text-text-tertiary')}>{option.text}</div>
{isActive && (
<div className='absolute bottom-0 left-0 right-0 h-0.5 bg-util-colors-blue-blue-500'></div>
<div className='absolute bottom-0 left-0 right-0 h-0.5 bg-util-colors-blue-brand-blue-brand-600'></div>
)}
</div>
)
}
interface Props {
type Props = {
className?: string
value: string
onChange: (v: string) => void
options: Option[]
noBorderBottom?: boolean
smallItem?: boolean
itemClassName?: string
}
@ -54,6 +58,7 @@ const TabSlider: FC<Props> = ({
options,
noBorderBottom,
itemClassName,
smallItem,
}) => {
return (
<div className={cn(className, !noBorderBottom && 'border-b border-divider-subtle', 'flex space-x-6')}>
@ -64,6 +69,7 @@ const TabSlider: FC<Props> = ({
onClick={onChange}
key={option.value}
className={itemClassName}
smallItem={smallItem}
/>
))}
</div>

View File

@ -22,6 +22,10 @@ import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-sele
import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import RadioE from '@/app/components/base/radio/ui'
import type {
NodeOutPutVar,
} from '@/app/components/workflow/types'
import type { Node } from 'reactflow'
type FormProps<
CustomFormSchema extends Omit<CredentialFormSchema, 'type'> & { type: string } = never,
@ -47,6 +51,9 @@ type FormProps<
) => ReactNode
// If return falsy value, this field will fallback to default render
override?: [Array<FormTypeEnum>, (formSchema: CredentialFormSchema, props: Omit<FormProps<CustomFormSchema>, 'override' | 'customRenderField'>) => ReactNode]
nodeId?: string
nodeOutputVars?: NodeOutPutVar[],
availableNodes?: Node[],
}
function Form<
@ -69,6 +76,9 @@ function Form<
fieldMoreInfo,
customRenderField,
override,
nodeId,
nodeOutputVars,
availableNodes,
}: FormProps<CustomFormSchema>) {
const language = useLanguage()
const [changeKey, setChangeKey] = useState('')
@ -326,6 +336,9 @@ function Form<
</div>
<ToolSelector
scope={scope}
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
disabled={readonly}
value={value[variable]}
// selectedTools={value[variable] ? [value[variable]] : []}
@ -351,6 +364,9 @@ function Form<
<div key={variable} className={cn(itemClassName, 'py-3')}>
<MultipleToolSelector
disabled={readonly}
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
scope={scope}
label={label[language] || label.en_US}
required={required}

View File

@ -10,6 +10,8 @@ import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import Divider from '@/app/components/base/divider'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
@ -21,6 +23,9 @@ type Props = {
supportCollapse?: boolean
scope?: string
onChange: (value: ToolValue[]) => void
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string
}
const MultipleToolSelector = ({
@ -32,6 +37,9 @@ const MultipleToolSelector = ({
supportCollapse,
scope,
onChange,
nodeOutputVars,
availableNodes,
nodeId,
}: Props) => {
const { t } = useTranslation()
const enabledCount = value.filter(item => item.enabled).length
@ -121,6 +129,9 @@ const MultipleToolSelector = ({
{!collapse && (
<>
<ToolSelector
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
scope={scope}
value={undefined}
selectedTools={value}
@ -140,6 +151,9 @@ const MultipleToolSelector = ({
{value.length > 0 && value.map((item, index) => (
<div className='mb-1' key={index}>
<ToolSelector
nodeId={nodeId}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
scope={scope}
value={item}
selectedTools={value}

View File

@ -21,8 +21,10 @@ import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/too
import Toast from '@/app/components/base/toast'
import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain'
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useAppContext } from '@/context/app-context'
import {
@ -41,6 +43,8 @@ import type {
Placement,
} from '@floating-ui/react'
import { MARKETPLACE_API_PREFIX } from '@/config'
import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
@ -54,6 +58,7 @@ type Props = {
provider_name: string
tool_name: string
tool_label: string
settings?: Record<string, any>
parameters?: Record<string, any>
extra?: Record<string, any>
}) => void
@ -65,6 +70,9 @@ type Props = {
onControlledStateChange?: (state: boolean) => void
panelShowState?: boolean
onPanelShowStateChange?: (state: boolean) => void
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId?: string,
}
const ToolSelector: FC<Props> = ({
value,
@ -81,6 +89,9 @@ const ToolSelector: FC<Props> = ({
onControlledStateChange,
panelShowState,
onPanelShowStateChange,
nodeOutputVars,
availableNodes,
nodeId = '',
}) => {
const { t } = useTranslation()
const [isShow, onShowChange] = useState(false)
@ -107,17 +118,20 @@ const ToolSelector: FC<Props> = ({
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const handleSelectTool = (tool: ToolDefaultValue) => {
const paramValues = addDefaultValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any))
const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true)
const toolValue = {
provider_name: tool.provider_id,
type: tool.provider_type,
tool_name: tool.tool_name,
tool_label: tool.tool_label,
settings: settingValues,
parameters: paramValues,
enabled: tool.is_team_authorization,
extra: {
description: '',
},
schemas: tool.paramSchemas,
}
onSelect(toolValue)
// setIsShowChooseTool(false)
@ -133,14 +147,33 @@ const ToolSelector: FC<Props> = ({
} as any)
}
const currentToolParams = useMemo(() => {
// tool settings & params
const currentToolSettings = useMemo(() => {
if (!currentProvider) return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form !== 'llm') || []
}, [currentProvider, value])
const currentToolParams = useMemo(() => {
if (!currentProvider) return []
return currentProvider.tools.find(tool => tool.name === value?.tool_name)?.parameters.filter(param => param.form === 'llm') || []
}, [currentProvider, value])
const [currType, setCurrType] = useState('settings')
const showTabSlider = currentToolSettings.length > 0 && currentToolParams.length > 0
const userSettingsOnly = currentToolSettings.length > 0 && !currentToolParams.length
const reasoningConfigOnly = currentToolParams.length > 0 && !currentToolSettings.length
const formSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const settingsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolSettings), [currentToolSettings])
const paramsFormSchemas = useMemo(() => toolParametersToFormSchemas(currentToolParams), [currentToolParams])
const handleFormChange = (v: Record<string, any>) => {
const handleSettingsFormChange = (v: Record<string, any>) => {
const newValue = getStructureValue(v)
const toolValue = {
...value,
settings: newValue,
}
onSelect(toolValue as any)
}
const handleParamsFormChange = (v: Record<string, any>) => {
const toolValue = {
...value,
parameters: v,
@ -281,12 +314,9 @@ const ToolSelector: FC<Props> = ({
</div>
{/* authorization */}
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
<div className='px-4 pt-3 flex flex-col'>
<div className='flex items-center gap-2'>
<div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.auth')}</div>
<Divider bgStyle='gradient' className='grow' />
</div>
<div className='py-2'>
<>
<Divider className='my-1 w-full' />
<div className='px-4 py-2'>
{!currentProvider.is_team_authorization && (
<Button
variant='primary'
@ -309,37 +339,87 @@ const ToolSelector: FC<Props> = ({
</Button>
)}
</div>
</div>
</>
)}
{/* tool settings */}
{currentToolParams.length > 0 && currentProvider?.is_team_authorization && (
<div className='px-4 pt-3'>
<div className='flex items-center gap-2'>
<div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div>
<Divider bgStyle='gradient' className='grow' />
</div>
<div className='py-2'>
<Form
value={value?.parameters || {}}
onChange={handleFormChange}
formSchemas={formSchemas as any}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>)
: null}
{(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
<>
<Divider className='my-1 w-full' />
{/* tabs */}
{nodeId && showTabSlider && (
<TabSlider
className='shrink-0 mt-1 px-4'
itemClassName='py-3'
noBorderBottom
smallItem
value={currType}
onChange={(value) => {
setCurrType(value)
}}
options={[
{ value: 'settings', text: t('plugin.detailPanel.toolSelector.settings')! },
{ value: 'params', text: t('plugin.detailPanel.toolSelector.params')! },
]}
/>
</div>
</div>
)}
{nodeId && showTabSlider && currType === 'params' && (
<div className='px-4 py-2'>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div>
)}
{/* user settings only */}
{userSettingsOnly && (
<div className='p-4 pb-1'>
<div className='text-text-primary system-sm-semibold-uppercase'>{t('plugin.detailPanel.toolSelector.settings')}</div>
</div>
)}
{/* reasoning config only */}
{nodeId && reasoningConfigOnly && (
<div className='mb-1 p-4 pb-1'>
<div className='text-text-primary system-sm-semibold-uppercase'>{t('plugin.detailPanel.toolSelector.params')}</div>
<div className='pb-1'>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div>
</div>
)}
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className='px-4 py-2'>
<Form
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
formSchemas={settingsFormSchemas as any}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>)
: null}
/>
</div>
)}
{/* reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={nodeId}
/>
)}
</>
)}
</>
)}

View File

@ -0,0 +1,275 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import {
RiArrowRightUpLine,
} from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Node } from 'reactflow'
import type {
NodeOutPutVar,
ValueSelector,
Var,
} from '@/app/components/workflow/types'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
value: Record<string, any>
onChange: (val: Record<string, any>) => void
schemas: any[]
nodeOutputVars: NodeOutPutVar[],
availableNodes: Node[],
nodeId: string
}
const ReasoningConfigForm: React.FC<Props> = ({
value,
onChange,
schemas,
nodeOutputVars,
availableNodes,
nodeId,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const handleAutomatic = (key: string, val: any) => {
onChange({
...value,
[key]: {
value: val ? null : value[key]?.value,
auto: val ? 1 : 0,
},
})
}
const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
const handleInputFocus = useCallback((variable: string) => {
return (value: boolean) => {
setInputsIsFocus((prev) => {
return {
...prev,
[variable]: value,
}
})
}
}, [])
const handleNotMixedTypeChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string, varKindType: VarKindType) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable].value
if (target) {
target.type = varKindType
target.value = varValue
}
else {
draft[variable].value = {
type: varKindType,
value: varValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleMixedTypeChange = useCallback((variable: string) => {
return (itemValue: string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
const target = draft[variable].value
if (target) {
target.value = itemValue
}
else {
draft[variable].value = {
type: VarKindType.mixed,
value: itemValue,
}
}
})
onChange(newValue)
}
}, [value, onChange])
const handleFileChange = useCallback((variable: string) => {
return (varValue: ValueSelector | string) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
type: VarKindType.variable,
value: varValue,
}
})
onChange(newValue)
}
}, [value, onChange])
const handleAppChange = useCallback((variable: string) => {
return (app: {
app_id: string
inputs: Record<string, any>
files?: any[]
}) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = app as any
})
onChange(newValue)
}
}, [onChange, value])
const handleModelChange = useCallback((variable: string) => {
return (model: any) => {
const newValue = produce(value, (draft: ToolVarInputs) => {
draft[variable].value = {
...draft[variable].value,
...model,
} as any
})
onChange(newValue)
}
}, [onChange, value])
const renderField = (schema: any) => {
const {
variable,
label,
required,
tooltip,
type,
scope,
url,
} = schema
const auto = value[variable]?.auto
const tooltipContent = (tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
triggerClassName='ml-1 w-4 h-4'
asChild={false} />
))
const varInput = value[variable].value
const isNumber = type === FormTypeEnum.textNumber
const isSelect = type === FormTypeEnum.select
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
// const isToolSelector = type === FormTypeEnum.toolSelector
const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector
return (
<div key={variable} className='space-y-1'>
<div className='flex items-center justify-between py-2 system-sm-semibold text-text-secondary'>
<div className='flex items-center space-x-2'>
<span className={cn('text-text-secondary code-sm-semibold')}>{label[language] || label.en_US}</span>
{required && (
<span className='ml-1 text-red-500'>*</span>
)}
{tooltipContent}
</div>
<div className='flex items-center gap-1 px-2 py-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter cursor-pointer hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}>
<span className='text-text-secondary system-xs-medium'>{t('plugin.detailPanel.toolSelector.auto')}</span>
<Switch
size='xs'
defaultValue={!!auto}
onChange={val => handleAutomatic(variable, val)}
/>
</div>
</div>
{auto === 0 && (
<>
{isString && (
<Input
className={cn(inputsIsFocus[variable] ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'rounded-lg px-3 py-[6px] border')}
value={varInput?.value as string || ''}
onChange={handleMixedTypeChange(variable)}
nodesOutputVars={nodeOutputVars}
availableNodes={availableNodes}
onFocusChange={handleInputFocus(variable)}
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
/>
)}
{/* {isString && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || ''}
onChange={handleNotMixedTypeChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.number || varPayload.type === VarType.secret || varPayload.type === VarType.string}
/>
)} */}
{(isNumber || isSelect) && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}
isSupportConstantValue
filterVar={isNumber ? (varPayload: Var) => varPayload.type === schema._type : undefined}
availableVars={isSelect ? nodeOutputVars : undefined}
schema={schema}
/>
)}
{isFile && (
<VarReferencePicker
zIndex={1001}
readonly={false}
isShowNodeName
nodeId={nodeId}
value={varInput?.value || []}
onChange={handleFileChange(variable)}
defaultVarKindType={VarKindType.variable}
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
/>
)}
{isAppSelector && (
<AppSelector
disabled={false}
scope={scope || 'all'}
value={varInput as any}
onSelect={handleAppChange(variable)}
/>
)}
{isModelSelector && (
<ModelParameterModal
popupClassName='!w-[387px]'
isAdvancedMode
isInWorkflow
value={varInput as any}
setModel={handleModelChange(variable)}
scope={scope}
/>
)}
</>
)}
{url && (
<a
href={url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>
)}
</div>
)
}
return (
<div className='px-4 py-2 space-y-3'>
{schemas.map(schema => renderField(schema))}
</div>
)
}
export default ReasoningConfigForm

View File

@ -63,3 +63,34 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia
})
return newValues
}
export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => {
const newValues = {} as any
formSchemas.forEach((formSchema) => {
const itemValue = value[formSchema.variable]
if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) {
newValues[formSchema.variable] = {
...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }),
}
}
})
return newValues
}
export const getPlainValue = (value: Record<string, any>) => {
const plainValue = { ...value } as any
Object.keys(plainValue).forEach((key) => {
plainValue[key] = value[key].value
})
return plainValue
}
export const getStructureValue = (value: Record<string, any>) => {
const newValue = { ...value } as any
Object.keys(newValue).forEach((key) => {
newValue[key] = {
value: value[key],
}
})
return newValue
}

View File

@ -35,6 +35,7 @@ export type ToolValue = {
provider_name: string
tool_name: string
tool_label: string
settings?: Record<string, any>
parameters?: Record<string, any>
enabled?: boolean
extra?: Record<string, any>

View File

@ -184,7 +184,7 @@ export const useChecklistBeforePublish = () => {
}
return true
}, [nodesExtraData, notify, t, store, isChatMode, buildInTools, customTools, workflowTools, language])
}, [store, isChatMode, notify, t, buildInTools, customTools, workflowTools, language, nodesExtraData, strategyProviders])
return {
handleCheckBeforePublish,

View File

@ -36,6 +36,7 @@ export type AgentStrategyProps = {
onFormValueChange: (value: ToolVarInputs) => void
nodeOutputVars?: NodeOutPutVar[],
availableNodes?: Node[],
nodeId?: string
}
type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field
@ -46,7 +47,7 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'>
type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema
export const AgentStrategy = memo((props: AgentStrategyProps) => {
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes } = props
const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props
const { t } = useTranslation()
const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration)
const renderI18nObject = useRenderI18nObject()
@ -141,7 +142,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
]
const renderField: ComponentProps<typeof Form<CustomField>>['customRenderField'] = (schema, props) => {
switch (schema.type) {
case 'tool-selector': {
case FormTypeEnum.toolSelector: {
const value = props.value[schema.variable]
const onChange = (value: any) => {
props.onChange({ ...props.value, [schema.variable]: value })
@ -154,6 +155,9 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
tooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
>
<ToolSelector
nodeId={props.nodeId || ''}
nodeOutputVars={props.nodeOutputVars || []}
availableNodes={props.availableNodes || []}
scope={schema.scope}
value={value}
onSelect={item => onChange(item)}
@ -162,13 +166,16 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
</Field>
)
}
case 'array[tools]': {
case FormTypeEnum.multiToolSelector: {
const value = props.value[schema.variable]
const onChange = (value: any) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
return (
<MultipleToolSelector
nodeId={props.nodeId || ''}
nodeOutputVars={props.nodeOutputVars || []}
availableNodes={props.availableNodes || []}
scope={schema.scope}
value={value || []}
label={renderI18nObject(schema.label)}
@ -199,6 +206,9 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
fieldLabelClassName='uppercase'
customRenderField={renderField}
override={override}
nodeId={nodeId}
nodeOutputVars={nodeOutputVars || []}
availableNodes={availableNodes || []}
/>
</div>
: <ListEmpty

View File

@ -14,7 +14,7 @@ import type { ToolNodeType } from '../../../tool/types'
import type { ParameterExtractorNodeType } from '../../../parameter-extractor/types'
import type { IterationNodeType } from '../../../iteration/types'
import type { ListFilterNodeType } from '../../../list-operator/types'
import { OUTPUT_FILE_SUB_VARIABLES } from '../../../if-else/default'
import { OUTPUT_FILE_SUB_VARIABLES } from '../../../constants'
import type { DocExtractorNodeType } from '../../../document-extractor/types'
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'

View File

@ -64,6 +64,7 @@ type Props = {
placeholder?: string
minWidth?: number
popupFor?: 'assigned' | 'toAssigned'
zIndex?: number
}
const VarReferencePicker: FC<Props> = ({
@ -90,6 +91,7 @@ const VarReferencePicker: FC<Props> = ({
placeholder,
minWidth,
popupFor,
zIndex,
}) => {
const { t } = useTranslation()
const store = useStoreApi()
@ -386,7 +388,7 @@ const VarReferencePicker: FC<Props> = ({
</>
</WrapElem>
<PortalToFollowElemContent style={{
zIndex: 100,
zIndex: zIndex || 100,
}} className='mt-1'>
{!isConstant && (
<VarReferencePopup

View File

@ -2,6 +2,7 @@ import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plug
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '../../constants'
import type { NodeDefault } from '../../types'
import type { AgentNodeType } from './types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { renderI18nObject } from '@/hooks/use-i18n'
const nodeDefault: NodeDefault<AgentNodeType> = {
@ -37,6 +38,94 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
}
}
for (const param of strategy.parameters) {
// single tool
if (param.required && param.type === FormTypeEnum.toolSelector) {
// no value
const toolValue = payload.agent_parameters?.[param.name]?.value
if (!toolValue) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
}
}
// not enabled
else if (!toolValue.enabled) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.noValidTool', { field: renderI18nObject(param.label, language) }),
}
}
// check form of tool
else {
const schemas = toolValue.schemas
const userSettings = toolValue.settings
const reasoningConfig = toolValue.parameters
schemas.forEach((schema: any) => {
if (schema.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
}
})
}
}
// multiple tools
if (param.required && param.type === FormTypeEnum.multiToolSelector) {
const tools = payload.agent_parameters?.[param.name]?.value || []
// no value
if (!tools.length) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),
}
}
// not enabled
else if (tools.every((tool: any) => !tool.enabled)) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.noValidTool', { field: renderI18nObject(param.label, language) }),
}
}
// check form of tools
else {
let validState = {
isValid: true,
errorMessage: '',
}
for (const tool of tools) {
const schemas = tool.schemas
const userSettings = tool.settings
const reasoningConfig = tool.parameters
schemas.forEach((schema: any) => {
if (schema.required) {
if (schema.form === 'form' && !userSettings[schema.name]?.value) {
return validState = {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
if (schema.form === 'llm' && reasoningConfig[schema.name]?.auto === 0 && !reasoningConfig[schema.name]?.value) {
return validState = {
isValid: false,
errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }),
}
}
}
})
}
return validState
}
}
// common params
if (param.required && !payload.agent_parameters?.[param.name]?.value) {
return {
isValid: false,

View File

@ -103,6 +103,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
onFormValueChange={onFormChange}
nodeOutputVars={availableVars}
availableNodes={availableNodesWithParent}
nodeId={props.id}
/>
</Field>
<div>

View File

@ -36,6 +36,7 @@ import ListFilterNode from './list-operator/node'
import ListFilterPanel from './list-operator/panel'
import AgentNode from './agent/node'
import AgentPanel from './agent/panel'
import { TransferMethod } from '@/types/app'
export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Start]: StartNode,
@ -82,3 +83,18 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
}
export const CUSTOM_NODE_TYPE = 'custom'
export const FILE_TYPE_OPTIONS = [
{ value: 'image', i18nKey: 'image' },
{ value: 'document', i18nKey: 'doc' },
{ value: 'audio', i18nKey: 'audio' },
{ value: 'video', i18nKey: 'video' },
]
export const TRANSFER_METHOD = [
{ value: TransferMethod.local_file, i18nKey: 'localUpload' },
{ value: TransferMethod.remote_url, i18nKey: 'url' },
]
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')

View File

@ -9,7 +9,7 @@ import {
isComparisonOperatorNeedTranslate,
isEmptyRelatedOperator,
} from '../utils'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../../constants'
import type { ValueSelector } from '../../../types'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'

View File

@ -21,7 +21,7 @@ import {
} from '../../types'
import { comparisonOperatorNotRequireValue, getOperators } from '../../utils'
import ConditionNumberInput from '../condition-number-input'
import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../default'
import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../../constants'
import ConditionWrap from '../condition-wrap'
import ConditionOperator from './condition-operator'
import ConditionInput from './condition-input'
@ -39,7 +39,7 @@ import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
interface ConditionItemProps {
type ConditionItemProps = {
className?: string
disabled?: boolean
caseId: string

View File

@ -9,7 +9,7 @@ import {
comparisonOperatorNotRequireValue,
isComparisonOperatorNeedTranslate,
} from '../utils'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../../constants'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import cn from '@/utils/classnames'
@ -20,7 +20,7 @@ import type {
Node,
} from '@/app/components/workflow/types'
interface ConditionValueProps {
type ConditionValueProps = {
variableSelector: string[]
labelName?: string
operator: ComparisonOperator

View File

@ -12,7 +12,7 @@ import type { CaseItem, HandleAddCondition, HandleAddSubVariableCondition, Handl
import type { Node, NodeOutPutVar, Var } from '../../../types'
import { VarType } from '../../../types'
import { useGetAvailableVars } from '../../variable-assigner/hooks'
import { SUB_VARIABLES } from '../default'
import { SUB_VARIABLES } from '../../constants'
import ConditionList from './condition-list'
import ConditionAdd from './condition-add'
import cn from '@/utils/classnames'

View File

@ -1,7 +1,6 @@
import { BlockEnum, type NodeDefault } from '../../types'
import { type IfElseNodeType, LogicalOperator } from './types'
import { isEmptyRelatedOperator } from './utils'
import { TransferMethod } from '@/types/app'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'
@ -79,18 +78,3 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
}
export default nodeDefault
export const FILE_TYPE_OPTIONS = [
{ value: 'image', i18nKey: 'image' },
{ value: 'document', i18nKey: 'doc' },
{ value: 'audio', i18nKey: 'audio' },
{ value: 'video', i18nKey: 'video' },
]
export const TRANSFER_METHOD = [
{ value: TransferMethod.local_file, i18nKey: 'localUpload' },
{ value: TransferMethod.remote_url, i18nKey: 'url' },
]
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')

View File

@ -9,7 +9,7 @@ import { ComparisonOperator } from '../../if-else/types'
import { comparisonOperatorNotRequireValue, getOperators } from '../../if-else/utils'
import SubVariablePicker from './sub-variable-picker'
import Input from '@/app/components/base/input'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/if-else/default'
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/constants'
import { SimpleSelect as Select } from '@/app/components/base/select'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { SUB_VARIABLES } from '../../if-else/default'
import { SUB_VARIABLES } from '../../constants'
import type { Item } from '@/app/components/base/select'
import { SimpleSelect as Select } from '@/app/components/base/select'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'

View File

@ -78,8 +78,11 @@ const translation = {
descriptionLabel: 'Tool description',
descriptionPlaceholder: 'Brief description of the tool\'s purpose, e.g., get the temperature for a specific location.',
placeholder: 'Select a tool...',
auth: 'AUTHORIZATION',
settings: 'TOOL SETTINGS',
settings: 'USER SETTINGS',
params: 'REASONING CONFIG',
paramsTip1: 'Controls LLM inference parameters.',
paramsTip2: 'When \'Automatic\' is off, the default value is used.',
auto: 'Automatic',
empty: 'Click the \'+\' button to add tools. You can add multiple tools.',
uninstalledTitle: 'Tool not installed',
uninstalledContent: 'This plugin is installed from the local/GitHub repository. Please use after installation.',

View File

@ -195,6 +195,8 @@ const translation = {
visionVariable: 'Vision Variable',
},
invalidVariable: 'Invalid variable',
noValidTool: '{{field}} no valid tool selected',
toolParameterRequired: '{{field}}: parameter [{{param}}] is required',
},
singleRun: {
testRun: 'Test Run ',

View File

@ -78,8 +78,11 @@ const translation = {
descriptionLabel: '工具描述',
descriptionPlaceholder: '简要描述工具目的,例如,获取特定位置的温度。',
placeholder: '选择工具',
auth: '授权',
settings: '工具设置',
settings: '用户设置',
params: '推理配置',
paramsTip1: '控制 LLM 推理参数。',
paramsTip2: '当“自动”关闭时,使用默认值。',
auto: '自动',
empty: '点击 "+" 按钮添加工具。您可以添加多个工具。',
uninstalledTitle: '工具未安装',
uninstalledContent: '此插件安装自 本地 / GitHub 仓库,请安装后使用。',

View File

@ -195,6 +195,9 @@ const translation = {
visionVariable: '视觉变量',
},
invalidVariable: '无效的变量',
noValidTool: '{{field}} 无可用工具',
toolParameterRequired: '{{field}}: 参数 [{{param}}] 不能为空',
},
singleRun: {
testRun: '测试运行 ',