mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-15 22:06:52 +08:00
feat: workflow new nodes (#4683)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Patryk Garstecki <patryk20120@yahoo.pl> Co-authored-by: Sebastian.W <thiner@gmail.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: takatost <takatost@users.noreply.github.com> Co-authored-by: rechardwang <wh_goodjob@163.com> Co-authored-by: Nite Knite <nkCoding@gmail.com> Co-authored-by: Chenhe Gu <guchenhe@gmail.com> Co-authored-by: Joshua <138381132+joshua20231026@users.noreply.github.com> Co-authored-by: Weaxs <459312872@qq.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: leejoo0 <81673835+leejoo0@users.noreply.github.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Vikey Chen <vikeytk@gmail.com> Co-authored-by: wanghl <Wang-HL@users.noreply.github.com> Co-authored-by: Haolin Wang-汪皓临 <haolin.wang@atlaslovestravel.com> Co-authored-by: Zixuan Cheng <61724187+Theysua@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Bowen Liang <bowenliang@apache.org> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: fanghongtai <42790567+fanghongtai@users.noreply.github.com> Co-authored-by: wxfanghongtai <wxfanghongtai@gf.com.cn> Co-authored-by: Matri <qjp@bithuman.io> Co-authored-by: Benjamin <benjaminx@gmail.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BlockSelector from '../../../../block-selector'
|
||||
import type { Param, ParamType } from '../../types'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
|
||||
import type { ToolParameter } from '@/app/components/tools/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.parameterExtractor'
|
||||
|
||||
type Props = {
|
||||
onImport: (params: Param[]) => void
|
||||
}
|
||||
|
||||
function toParmExactParams(toolParams: ToolParameter[], lan: string): Param[] {
|
||||
return toolParams.map((item) => {
|
||||
return {
|
||||
name: item.name,
|
||||
type: item.type as ParamType,
|
||||
required: item.required,
|
||||
description: item.llm_description,
|
||||
options: item.options?.map(option => option.label[lan] || option.label.en_US) || [],
|
||||
}
|
||||
})
|
||||
}
|
||||
const ImportFromTool: FC<Props> = ({
|
||||
onImport,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
|
||||
const handleSelectTool = useCallback((_type: BlockEnum, toolInfo?: ToolDefaultValue) => {
|
||||
const { provider_id, provider_type, tool_name } = toolInfo!
|
||||
const currentTools = (() => {
|
||||
switch (provider_type) {
|
||||
case CollectionType.builtIn:
|
||||
return buildInTools
|
||||
case CollectionType.custom:
|
||||
return customTools
|
||||
case CollectionType.workflow:
|
||||
return workflowTools
|
||||
default:
|
||||
return []
|
||||
}
|
||||
})()
|
||||
const currCollection = currentTools.find(item => item.id === provider_id)
|
||||
const currTool = currCollection?.tools.find(tool => tool.name === tool_name)
|
||||
const toExactParams = (currTool?.parameters || []).filter((item: any) => item.form === 'llm')
|
||||
const formattedParams = toParmExactParams(toExactParams, language)
|
||||
onImport(formattedParams)
|
||||
}, [buildInTools, customTools, language, onImport, workflowTools])
|
||||
|
||||
const renderTrigger = useCallback((open: boolean) => {
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(
|
||||
'flex items-center h-6 px-2 cursor-pointer rounded-md hover:bg-gray-100 text-xs font-medium text-gray-500',
|
||||
open && 'bg-gray-100',
|
||||
)}>
|
||||
{t(`${i18nPrefix}.importFromTool`)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 52,
|
||||
}}
|
||||
trigger={renderTrigger}
|
||||
onSelect={handleSelectTool}
|
||||
noBlocks
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default memo(ImportFromTool)
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Param } from '../../types'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Edit03, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
const i18nPrefix = 'workflow.nodes.parameterExtractor'
|
||||
|
||||
type Props = {
|
||||
payload: Param
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
const Item: FC<Props> = ({
|
||||
payload,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='relative px-2.5 py-2 rounded-lg bg-white border-[0.5px] border-gray-200 hover:shadow-xs group'>
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Variable02 className='w-3.5 h-3.5 text-primary-500' />
|
||||
<div className='ml-1 text-[13px] font-medium text-gray-900'>{payload.name}</div>
|
||||
<div className='ml-2 text-xs font-normal text-gray-500 capitalize'>{payload.type}</div>
|
||||
</div>
|
||||
{payload.required && (
|
||||
<div className='uppercase leading-4 text-xs font-normal text-gray-500'>{t(`${i18nPrefix}.addExtractParameterContent.required`)}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-0.5 leading-[18px] text-xs font-normal text-gray-500'>{payload.description}</div>
|
||||
<div
|
||||
className='group-hover:flex absolute top-0 right-1 hidden h-full items-center w-[119px] justify-end space-x-1 rounded-lg'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, #FFF 49.99%, rgba(255, 255, 255, 0.00) 98.1%)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Edit03 className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash03 className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Item)
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { Param } from '../../types'
|
||||
import ListNoDataPlaceholder from '../../../_base/components/list-no-data-placeholder'
|
||||
import Item from './item'
|
||||
import EditParam from './update'
|
||||
import type { MoreInfo } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.parameterExtractor'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
list: Param[]
|
||||
onChange: (list: Param[], moreInfo?: MoreInfo) => void
|
||||
}
|
||||
|
||||
const List: FC<Props> = ({
|
||||
list,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowEditModal, {
|
||||
setTrue: showEditModal,
|
||||
setFalse: hideEditModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleItemChange = useCallback((index: number) => {
|
||||
return (payload: Param, moreInfo?: MoreInfo) => {
|
||||
const newList = list.map((item, i) => {
|
||||
if (i === index)
|
||||
return payload
|
||||
|
||||
return item
|
||||
})
|
||||
onChange(newList, moreInfo)
|
||||
hideEditModal()
|
||||
}
|
||||
}, [hideEditModal, list, onChange])
|
||||
|
||||
const [currEditItemIndex, setCurrEditItemIndex] = useState<number>(-1)
|
||||
|
||||
const handleItemEdit = useCallback((index: number) => {
|
||||
return () => {
|
||||
setCurrEditItemIndex(index)
|
||||
showEditModal()
|
||||
}
|
||||
}, [showEditModal])
|
||||
|
||||
const handleItemDelete = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newList = list.filter((_, i) => i !== index)
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
if (list.length === 0) {
|
||||
return (
|
||||
<ListNoDataPlaceholder >{t(`${i18nPrefix}.extractParametersNotSet`)}</ListNoDataPlaceholder>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className='space-y-1'>
|
||||
{list.map((item, index) => (
|
||||
<Item
|
||||
key={index}
|
||||
payload={item}
|
||||
onDelete={handleItemDelete(index)}
|
||||
onEdit={handleItemEdit(index)}
|
||||
/>
|
||||
))}
|
||||
{isShowEditModal && (
|
||||
<EditParam
|
||||
type='edit'
|
||||
payload={list[currEditItemIndex]}
|
||||
onSave={handleItemChange(currEditItemIndex)}
|
||||
onCancel={hideEditModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(List)
|
||||
@@ -0,0 +1,193 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import type { Param } from '../../types'
|
||||
import { ParamType } from '../../types'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import ConfigSelect from '@/app/components/app/configuration/config-var/config-select'
|
||||
import { ChangeType, type MoreInfo } from '@/app/components/workflow/types'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.parameterExtractor'
|
||||
const errorI18nPrefix = 'workflow.errorMsg'
|
||||
const inputClassName = 'w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
|
||||
const DEFAULT_PARAM: Param = {
|
||||
name: '',
|
||||
type: ParamType.string,
|
||||
description: '',
|
||||
required: false,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type: 'add' | 'edit'
|
||||
payload?: Param
|
||||
onSave: (payload: Param, moreInfo?: MoreInfo) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
const TYPES = [ParamType.string, ParamType.number, ParamType.arrayString, ParamType.arrayNumber, ParamType.arrayObject]
|
||||
|
||||
const AddExtractParameter: FC<Props> = ({
|
||||
type,
|
||||
payload,
|
||||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isAdd = type === 'add'
|
||||
const [param, setParam] = useState<Param>(isAdd ? DEFAULT_PARAM : payload as Param)
|
||||
const [renameInfo, setRenameInfo] = useState<MoreInfo | undefined>(undefined)
|
||||
const handleParamChange = useCallback((key: string) => {
|
||||
return (value: any) => {
|
||||
if (key === 'name') {
|
||||
const { isValid, errorKey, errorMessageKey } = checkKeys([value], true)
|
||||
if (!isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
setRenameInfo(key === 'name'
|
||||
? {
|
||||
type: ChangeType.changeVarName,
|
||||
payload: {
|
||||
beforeKey: param.name,
|
||||
afterKey: value,
|
||||
},
|
||||
}
|
||||
: undefined)
|
||||
setParam((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[key]: value,
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [param.name, t])
|
||||
|
||||
const [isShowModal, {
|
||||
setTrue: doShowModal,
|
||||
setFalse: doHideModal,
|
||||
}] = useBoolean(!isAdd)
|
||||
|
||||
const hideModal = useCallback(() => {
|
||||
doHideModal()
|
||||
onCancel?.()
|
||||
}, [onCancel, doHideModal])
|
||||
|
||||
const showAddModal = useCallback(() => {
|
||||
if (isAdd)
|
||||
setParam(DEFAULT_PARAM)
|
||||
|
||||
doShowModal()
|
||||
}, [isAdd, doShowModal])
|
||||
|
||||
const checkValid = useCallback(() => {
|
||||
let errMessage = ''
|
||||
if (!param.name)
|
||||
errMessage = t(`${errorI18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.addExtractParameterContent.name`) })
|
||||
if (!errMessage && param.type === ParamType.select && (!param.options || param.options.length === 0))
|
||||
errMessage = t(`${errorI18nPrefix}.fieldRequired`, { field: t('appDebug.variableConig.options') })
|
||||
if (!errMessage && !param.description)
|
||||
errMessage = t(`${errorI18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.addExtractParameterContent.description`) })
|
||||
|
||||
if (errMessage) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errMessage,
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, [param, t])
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (!checkValid())
|
||||
return
|
||||
|
||||
onSave(param, renameInfo)
|
||||
hideModal()
|
||||
}, [checkValid, onSave, param, hideModal, renameInfo])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isAdd && (
|
||||
<AddButton className='mx-1' onClick={showAddModal} />
|
||||
)}
|
||||
{isShowModal && (
|
||||
<Modal
|
||||
title={t(`${i18nPrefix}.addExtractParameter`)}
|
||||
isShow
|
||||
onClose={hideModal}
|
||||
className='!w-[400px] !max-w-[400px] !p-4'
|
||||
wrapperClassName='!z-[100]'
|
||||
>
|
||||
<div>
|
||||
<div className='space-y-2'>
|
||||
<Field title={t(`${i18nPrefix}.addExtractParameterContent.name`)}>
|
||||
<input
|
||||
type='text'
|
||||
className={inputClassName}
|
||||
value={param.name}
|
||||
onChange={e => handleParamChange('name')(e.target.value)}
|
||||
placeholder={t(`${i18nPrefix}.addExtractParameterContent.namePlaceholder`)!}
|
||||
/>
|
||||
</Field>
|
||||
<Field title={t(`${i18nPrefix}.addExtractParameterContent.type`)}>
|
||||
<Select
|
||||
defaultValue={param.type}
|
||||
allowSearch={false}
|
||||
bgClassName='bg-gray-100'
|
||||
onSelect={v => handleParamChange('type')(v.value)}
|
||||
optionClassName='capitalize'
|
||||
items={
|
||||
TYPES.map(type => ({
|
||||
value: type,
|
||||
name: type,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
{param.type === ParamType.select && (
|
||||
<Field title={t('appDebug.variableConig.options')}>
|
||||
<ConfigSelect options={param.options || []} onChange={handleParamChange('options')} />
|
||||
</Field>
|
||||
)}
|
||||
<Field title={t(`${i18nPrefix}.addExtractParameterContent.description`)}>
|
||||
<textarea
|
||||
className={cn(inputClassName, '!h-[80px]')}
|
||||
value={param.description}
|
||||
onChange={e => handleParamChange('description')(e.target.value)}
|
||||
placeholder={t(`${i18nPrefix}.addExtractParameterContent.descriptionPlaceholder`)!}
|
||||
/>
|
||||
</Field>
|
||||
<Field title={t(`${i18nPrefix}.addExtractParameterContent.required`)}>
|
||||
<>
|
||||
<div className='mb-1.5 leading-[18px] text-xs font-normal text-gray-500'>{t(`${i18nPrefix}.addExtractParameterContent.requiredContent`)}</div>
|
||||
<Switch size='l' defaultValue={param.required} onChange={handleParamChange('required')} />
|
||||
</>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='mt-4 flex justify-end space-x-2'>
|
||||
<Button className='flex !h-8 !w-[95px] text-[13px] font-medium text-gray-700' onClick={hideModal} >{t('common.operation.cancel')}</Button>
|
||||
<Button className='flex !h-8 !w-[95px] text-[13px] font-medium' type='primary' onClick={handleSave} >{isAdd ? t('common.operation.add') : t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AddExtractParameter)
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReasoningModeType } from '../types'
|
||||
import Field from '../../_base/components/field'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.parameterExtractor'
|
||||
|
||||
type ItemProps = {
|
||||
isChosen: boolean
|
||||
text: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
isChosen,
|
||||
text,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(isChosen ? 'border-[1.5px] border-primary-400 bg-white' : 'border border-gray-100 bg-gray-25', 'grow w-0 shrink-0 flex items-center h-8 justify-center rounded-lg cursor-pointer text-[13px] font-normal text-gray-900')}
|
||||
onClick={() => !isChosen ? onClick() : () => { }}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type: ReasoningModeType
|
||||
onChange: (type: ReasoningModeType) => void
|
||||
}
|
||||
|
||||
const ReasoningModePicker: FC<Props> = ({
|
||||
type,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleChange = useCallback((type: ReasoningModeType) => {
|
||||
return () => {
|
||||
onChange(type)
|
||||
}
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.reasoningMode`)}
|
||||
tooltip={t(`${i18nPrefix}.reasoningModeTip`)!}
|
||||
>
|
||||
<div className='flex space-x-1'>
|
||||
<Item
|
||||
isChosen={type === ReasoningModeType.functionCall}
|
||||
text='Function/Tool Calling'
|
||||
onClick={handleChange(ReasoningModeType.functionCall)}
|
||||
/>
|
||||
<Item
|
||||
isChosen={type === ReasoningModeType.prompt}
|
||||
text='Prompt'
|
||||
onClick={handleChange(ReasoningModeType.prompt)}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(ReasoningModePicker)
|
||||
Reference in New Issue
Block a user