dify
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiFileCopyLine } from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { debounce } from 'lodash-es'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type Props = {
|
||||
content: string
|
||||
}
|
||||
|
||||
const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
|
||||
|
||||
const CopyFeedbackNew = ({ content }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
copy(content)
|
||||
setIsCopied(true)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
|
||||
return (
|
||||
<div className='inline-flex w-full pb-0.5' onClick={e => e.stopPropagation()} onMouseLeave={onMouseLeave}>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
(isCopied
|
||||
? t(`${prefixEmbedded}.copied`)
|
||||
: t(`${prefixEmbedded}.copy`)) || ''
|
||||
}
|
||||
>
|
||||
<div
|
||||
className='group/copy flex w-full items-center gap-0.5 '
|
||||
onClick={onClickCopy}
|
||||
>
|
||||
<div
|
||||
className='system-2xs-regular w-0 grow cursor-pointer truncate text-text-quaternary group-hover:text-text-tertiary'
|
||||
>{content}</div>
|
||||
<RiFileCopyLine className='h-3 w-3 shrink-0 text-text-tertiary opacity-0 group-hover/copy:opacity-100' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyFeedbackNew
|
||||
@@ -0,0 +1,258 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ToolVarInputs } from '../types'
|
||||
import { VarType as VarKindType } from '../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
|
||||
import { noop } from 'lodash-es'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema[]
|
||||
value: ToolVarInputs
|
||||
onChange: (value: ToolVarInputs) => void
|
||||
onOpen?: (index: number) => void
|
||||
isSupportConstantValue?: boolean
|
||||
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
}
|
||||
|
||||
const InputVarList: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
schema,
|
||||
value,
|
||||
onChange,
|
||||
onOpen = noop,
|
||||
isSupportConstantValue,
|
||||
filterVar,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const { t } = useTranslation()
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
const paramType = (type: string) => {
|
||||
if (type === FormTypeEnum.textNumber)
|
||||
return 'Number'
|
||||
else if (type === FormTypeEnum.file || type === FormTypeEnum.files)
|
||||
return 'Files'
|
||||
else if (type === FormTypeEnum.appSelector)
|
||||
return 'AppSelector'
|
||||
else if (type === FormTypeEnum.modelSelector)
|
||||
return 'ModelSelector'
|
||||
else if (type === FormTypeEnum.toolSelector)
|
||||
return 'ToolSelector'
|
||||
else if (type === FormTypeEnum.dynamicSelect || type === FormTypeEnum.select)
|
||||
return 'Select'
|
||||
else
|
||||
return 'String'
|
||||
}
|
||||
|
||||
const handleNotMixedTypeChange = useCallback((variable: string) => {
|
||||
return (varValue: ValueSelector | string, varKindType: VarKindType) => {
|
||||
const newValue = produce(value, (draft: ToolVarInputs) => {
|
||||
const target = draft[variable]
|
||||
if (target) {
|
||||
target.type = varKindType
|
||||
target.value = varValue
|
||||
}
|
||||
else {
|
||||
draft[variable] = {
|
||||
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]
|
||||
if (target) {
|
||||
target.value = itemValue
|
||||
}
|
||||
else {
|
||||
draft[variable] = {
|
||||
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] = {
|
||||
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] = app as any
|
||||
})
|
||||
onChange(newValue)
|
||||
}
|
||||
}, [onChange, value])
|
||||
const handleModelChange = useCallback((variable: string) => {
|
||||
return (model: any) => {
|
||||
const newValue = produce(value, (draft: ToolVarInputs) => {
|
||||
draft[variable] = {
|
||||
...draft[variable],
|
||||
...model,
|
||||
} as any
|
||||
})
|
||||
onChange(newValue)
|
||||
}
|
||||
}, [onChange, value])
|
||||
|
||||
const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({})
|
||||
const handleInputFocus = useCallback((variable: string) => {
|
||||
return (value: boolean) => {
|
||||
setInputsIsFocus((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[variable]: value,
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
const handleOpen = useCallback((index: number) => {
|
||||
return () => onOpen(index)
|
||||
}, [onOpen])
|
||||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
{
|
||||
schema.map((schema, index) => {
|
||||
const {
|
||||
variable,
|
||||
label,
|
||||
type,
|
||||
required,
|
||||
tooltip,
|
||||
scope,
|
||||
} = schema
|
||||
const varInput = value[variable]
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
|
||||
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
|
||||
const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
const isModelSelector = type === FormTypeEnum.modelSelector
|
||||
// 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 space-x-2 leading-[18px]'>
|
||||
<span className='code-sm-semibold text-text-secondary'>{label[language] || label.en_US}</span>
|
||||
<span className='system-xs-regular text-text-tertiary'>{paramType(type)}</span>
|
||||
{required && <span className='system-xs-regular text-util-colors-orange-dark-orange-dark-600'>Required</span>}
|
||||
</div>
|
||||
{isString && (
|
||||
<Input
|
||||
className={cn(inputsIsFocus[variable] ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'rounded-lg border px-3 py-[6px]')}
|
||||
value={varInput?.value as string || ''}
|
||||
onChange={handleMixedTypeChange(variable)}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
onFocusChange={handleInputFocus(variable)}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
)}
|
||||
{(isNumber || isSelect) && (
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
|
||||
onChange={handleNotMixedTypeChange(variable)}
|
||||
onOpen={handleOpen(index)}
|
||||
defaultVarKindType={varInput?.type || ((isNumber || isDynamicSelect) ? VarKindType.constant : VarKindType.variable)}
|
||||
isSupportConstantValue={isSupportConstantValue}
|
||||
filterVar={isNumber ? filterVar : undefined}
|
||||
availableVars={isSelect ? availableVars : undefined}
|
||||
schema={schema}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
)}
|
||||
{isFile && (
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.value || []}
|
||||
onChange={handleFileChange(variable)}
|
||||
onOpen={handleOpen(index)}
|
||||
defaultVarKindType={VarKindType.variable}
|
||||
filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile}
|
||||
/>
|
||||
)}
|
||||
{isAppSelector && (
|
||||
<AppSelector
|
||||
disabled={readOnly}
|
||||
scope={scope || 'all'}
|
||||
value={varInput as any}
|
||||
onSelect={handleAppChange(variable)}
|
||||
/>
|
||||
)}
|
||||
{isModelSelector && (
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[387px]'
|
||||
isAdvancedMode
|
||||
isInWorkflow
|
||||
value={varInput as any}
|
||||
setModel={handleModelChange(variable)}
|
||||
readonly={readOnly}
|
||||
scope={scope}
|
||||
/>
|
||||
)}
|
||||
{tooltip && <div className='body-xs-regular text-text-tertiary'>{tooltip[language] || tooltip.en_US}</div>}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputVarList)
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import Placeholder from './placeholder'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
type MixedVariableTextInputProps = {
|
||||
readOnly?: boolean
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
value?: string
|
||||
onChange?: (text: string) => void
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
disableVariableInsertion?: boolean
|
||||
}
|
||||
const MixedVariableTextInput = ({
|
||||
readOnly = false,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
value = '',
|
||||
onChange,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
disableVariableInsertion = false,
|
||||
}: MixedVariableTextInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
|
||||
|
||||
return (
|
||||
<PromptEditor
|
||||
key={controlPromptEditorRerenderKey}
|
||||
wrapperClassName={cn(
|
||||
'min-h-8 w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
|
||||
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
|
||||
)}
|
||||
className='caret:text-text-accent'
|
||||
editable={!readOnly}
|
||||
value={value}
|
||||
workflowVariableBlock={{
|
||||
show: !disableVariableInsertion,
|
||||
variables: nodesOutputVars || [],
|
||||
workflowNodesMap: availableNodes.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
title: node.data.title,
|
||||
type: node.data.type,
|
||||
}
|
||||
if (node.data.type === BlockEnum.Start) {
|
||||
acc.sys = {
|
||||
title: t('workflow.blocks.start'),
|
||||
type: BlockEnum.Start,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as any),
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
}}
|
||||
placeholder={<Placeholder disableVariableInsertion={disableVariableInsertion} />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MixedVariableTextInput)
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { FOCUS_COMMAND } from 'lexical'
|
||||
import { $insertNodes } from 'lexical'
|
||||
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
type PlaceholderProps = {
|
||||
disableVariableInsertion?: boolean
|
||||
}
|
||||
|
||||
const Placeholder = ({ disableVariableInsertion = false }: PlaceholderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
const handleInsert = useCallback((text: string) => {
|
||||
editor.update(() => {
|
||||
const textNode = new CustomTextNode(text)
|
||||
$insertNodes([textNode])
|
||||
})
|
||||
editor.dispatchCommand(FOCUS_COMMAND, undefined as any)
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleInsert('')
|
||||
}}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
{t('workflow.nodes.tool.insertPlaceholder1')}
|
||||
{(!disableVariableInsertion) && (
|
||||
<>
|
||||
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
|
||||
<div
|
||||
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
|
||||
onMouseDown={((e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleInsert('/')
|
||||
})}
|
||||
>
|
||||
{t('workflow.nodes.tool.insertPlaceholder2')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Badge
|
||||
className='shrink-0'
|
||||
text='String'
|
||||
uppercase={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Placeholder
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ToolVarInputs } from '../../types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import ToolFormItem from './item'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema[]
|
||||
value: ToolVarInputs
|
||||
onChange: (value: ToolVarInputs) => void
|
||||
onOpen?: (index: number) => void
|
||||
inPanel?: boolean
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
extraParams?: Record<string, any>
|
||||
}
|
||||
|
||||
const ToolForm: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
schema,
|
||||
value,
|
||||
onChange,
|
||||
inPanel,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
extraParams,
|
||||
}) => {
|
||||
return (
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
schema.map((schema, index) => (
|
||||
<ToolFormItem
|
||||
key={index}
|
||||
readOnly={readOnly}
|
||||
nodeId={nodeId}
|
||||
schema={schema}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
inPanel={inPanel}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
showManageInputField={showManageInputField}
|
||||
onManageInputField={onManageInputField}
|
||||
extraParams={extraParams}
|
||||
providerType='tool'
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ToolForm
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
RiBracesLine,
|
||||
} from '@remixicon/react'
|
||||
import type { ToolVarInputs } from '../../types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
schema: CredentialFormSchema
|
||||
value: ToolVarInputs
|
||||
onChange: (value: ToolVarInputs) => void
|
||||
inPanel?: boolean
|
||||
currentTool?: Tool
|
||||
currentProvider?: ToolWithProvider
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
extraParams?: Record<string, any>
|
||||
providerType?: 'tool' | 'trigger'
|
||||
}
|
||||
|
||||
const ToolFormItem: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
schema,
|
||||
value,
|
||||
onChange,
|
||||
inPanel,
|
||||
currentTool,
|
||||
currentProvider,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
extraParams,
|
||||
providerType = 'tool',
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const { name, label, type, required, tooltip, input_schema } = schema
|
||||
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
|
||||
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const [isShowSchema, {
|
||||
setTrue: showSchema,
|
||||
setFalse: hideSchema,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<div className='space-y-0.5 py-1'>
|
||||
<div>
|
||||
<div className='flex h-6 items-center'>
|
||||
<div className='system-sm-medium text-text-secondary'>{label[language] || label.en_US}</div>
|
||||
{required && (
|
||||
<div className='system-xs-regular ml-1 text-text-destructive-secondary'>*</div>
|
||||
)}
|
||||
{!showDescription && tooltip && (
|
||||
<Tooltip
|
||||
popupContent={<div className='w-[200px]'>
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
</div>}
|
||||
triggerClassName='ml-1 w-4 h-4'
|
||||
asChild={false}
|
||||
/>
|
||||
)}
|
||||
{showSchemaButton && (
|
||||
<>
|
||||
<div className='system-xs-regular ml-1 mr-0.5 text-text-quaternary'>·</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
onClick={showSchema}
|
||||
className='system-xs-regular px-1 text-text-tertiary'
|
||||
>
|
||||
<RiBracesLine className='mr-1 size-3.5' />
|
||||
<span>JSON Schema</span>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showDescription && tooltip && (
|
||||
<div className='body-xs-regular pb-0.5 text-text-tertiary'>{tooltip[language] || tooltip.en_US}</div>
|
||||
)}
|
||||
</div>
|
||||
<FormInputItem
|
||||
readOnly={readOnly}
|
||||
nodeId={nodeId}
|
||||
schema={schema}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
inPanel={inPanel}
|
||||
currentTool={currentTool}
|
||||
currentProvider={currentProvider}
|
||||
showManageInputField={showManageInputField}
|
||||
onManageInputField={onManageInputField}
|
||||
extraParams={extraParams}
|
||||
providerType={providerType}
|
||||
/>
|
||||
|
||||
{isShowSchema && (
|
||||
<SchemaModal
|
||||
isShow
|
||||
onClose={hideSchema}
|
||||
rootName={name}
|
||||
schema={input_schema!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ToolFormItem
|
||||
130
dify/web/app/components/workflow/nodes/tool/default.ts
Normal file
130
dify/web/app/components/workflow/nodes/tool/default.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import type { NodeDefault, ToolWithProvider } from '../../types'
|
||||
import type { ToolNodeType } from './types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { TOOL_OUTPUT_STRUCT } from '../../constants'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { getMatchedSchemaType } from '../_base/components/variable/use-match-schema-type'
|
||||
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const metaData = genNodeMetaData({
|
||||
sort: -1,
|
||||
type: BlockEnum.Tool,
|
||||
helpLinkUri: 'tools',
|
||||
})
|
||||
const nodeDefault: NodeDefault<ToolNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {
|
||||
tool_parameters: {},
|
||||
tool_configurations: {},
|
||||
tool_node_version: '2',
|
||||
},
|
||||
checkValid(payload: ToolNodeType, t: any, moreDataForCheckValid: any) {
|
||||
const { toolInputsSchema, toolSettingSchema, language, notAuthed } = moreDataForCheckValid
|
||||
let errorMessages = ''
|
||||
if (notAuthed)
|
||||
errorMessages = t(`${i18nPrefix}.authRequired`)
|
||||
|
||||
if (!errorMessages) {
|
||||
toolInputsSchema.filter((field: any) => {
|
||||
return field.required
|
||||
}).forEach((field: any) => {
|
||||
const targetVar = payload.tool_parameters[field.variable]
|
||||
if (!targetVar) {
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
|
||||
return
|
||||
}
|
||||
const { type: variable_type, value } = targetVar
|
||||
if (variable_type === VarKindType.variable) {
|
||||
if (!errorMessages && (!value || value.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
|
||||
}
|
||||
else {
|
||||
if (!errorMessages && (value === undefined || value === null || value === ''))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!errorMessages) {
|
||||
toolSettingSchema.filter((field: any) => {
|
||||
return field.required
|
||||
}).forEach((field: any) => {
|
||||
const value = payload.tool_configurations[field.variable]
|
||||
if (!errorMessages && (value === undefined || value === null || value === ''))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] })
|
||||
if (!errorMessages && typeof value === 'object' && !!value.type && (value.value === undefined || value.value === null || value.value === '' || (Array.isArray(value.value) && value.value.length === 0)))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] })
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
getOutputVars(payload: ToolNodeType, allPluginInfoList: Record<string, ToolWithProvider[]>, _ragVars: any, { schemaTypeDefinitions } = { schemaTypeDefinitions: [] }) {
|
||||
const { provider_id, provider_type } = payload
|
||||
let currentTools: ToolWithProvider[] = []
|
||||
switch (provider_type) {
|
||||
case CollectionType.builtIn:
|
||||
currentTools = allPluginInfoList.buildInTools ?? []
|
||||
break
|
||||
case CollectionType.custom:
|
||||
currentTools = allPluginInfoList.customTools ?? []
|
||||
break
|
||||
case CollectionType.workflow:
|
||||
currentTools = allPluginInfoList.workflowTools ?? []
|
||||
break
|
||||
case CollectionType.mcp:
|
||||
currentTools = allPluginInfoList.mcpTools ?? []
|
||||
break
|
||||
default:
|
||||
currentTools = []
|
||||
}
|
||||
const currCollection = currentTools.find(item => canFindTool(item.id, provider_id))
|
||||
const currTool = currCollection?.tools.find(tool => tool.name === payload.tool_name)
|
||||
const output_schema = currTool?.output_schema
|
||||
let res: any[] = []
|
||||
if (!output_schema || !output_schema.properties) {
|
||||
res = TOOL_OUTPUT_STRUCT
|
||||
}
|
||||
else {
|
||||
const outputSchema: any[] = []
|
||||
Object.keys(output_schema.properties).forEach((outputKey) => {
|
||||
const output = output_schema.properties[outputKey]
|
||||
const dataType = output.type
|
||||
const schemaType = getMatchedSchemaType(output, schemaTypeDefinitions)
|
||||
let type = dataType === 'array'
|
||||
? `Array[${output.items?.type ? output.items.type.slice(0, 1).toLocaleLowerCase() + output.items.type.slice(1) : 'Unknown'}]`
|
||||
: `${output.type ? output.type.slice(0, 1).toLocaleLowerCase() + output.type.slice(1) : 'Unknown'}`
|
||||
|
||||
if (type === VarType.object && schemaType === 'file')
|
||||
type = VarType.file
|
||||
|
||||
outputSchema.push({
|
||||
variable: outputKey,
|
||||
type,
|
||||
description: output.description,
|
||||
schemaType,
|
||||
children: output.type === 'object' ? {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: output.properties,
|
||||
},
|
||||
} : undefined,
|
||||
})
|
||||
})
|
||||
res = [
|
||||
...TOOL_OUTPUT_STRUCT,
|
||||
...outputSchema,
|
||||
]
|
||||
}
|
||||
return res
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
92
dify/web/app/components/workflow/nodes/tool/node.tsx
Normal file
92
dify/web/app/components/workflow/nodes/tool/node.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button'
|
||||
import { useNodePluginInstallation } from '@/app/components/workflow/hooks/use-node-plugin-installation'
|
||||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks/use-node-data-update'
|
||||
import type { ToolNodeType } from './types'
|
||||
|
||||
const Node: FC<NodeProps<ToolNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { tool_configurations, paramSchemas } = data
|
||||
const toolConfigs = Object.keys(tool_configurations || {})
|
||||
const {
|
||||
isChecking,
|
||||
isMissing,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
shouldDim,
|
||||
} = useNodePluginInstallation(data)
|
||||
const showInstallButton = !isChecking && isMissing && canInstall && uniqueIdentifier
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const shouldLock = !isChecking && isMissing && canInstall && Boolean(uniqueIdentifier)
|
||||
|
||||
useEffect(() => {
|
||||
if (data._pluginInstallLocked === shouldLock && data._dimmed === shouldDim)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
_pluginInstallLocked: shouldLock,
|
||||
_dimmed: shouldDim,
|
||||
},
|
||||
})
|
||||
}, [data._pluginInstallLocked, data._dimmed, handleNodeDataUpdate, id, shouldDim, shouldLock])
|
||||
|
||||
const hasConfigs = toolConfigs.length > 0
|
||||
|
||||
if (!showInstallButton && !hasConfigs)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='relative mb-1 px-3 py-1'>
|
||||
{showInstallButton && (
|
||||
<div className='pointer-events-auto absolute right-3 top-[-32px] z-40'>
|
||||
<InstallPluginButton
|
||||
size='small'
|
||||
className='!font-medium !text-text-accent'
|
||||
extraIdentifiers={[
|
||||
data.plugin_id,
|
||||
data.provider_id,
|
||||
data.provider_name,
|
||||
].filter(Boolean) as string[]}
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
onSuccess={onInstallSuccess}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasConfigs && (
|
||||
<div className='space-y-0.5' aria-disabled={shouldDim}>
|
||||
{toolConfigs.map((key, index) => (
|
||||
<div key={index} className='flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary'>
|
||||
<div title={key} className='max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary'>
|
||||
{key}
|
||||
</div>
|
||||
{typeof tool_configurations[key].value === 'string' && (
|
||||
<div title={tool_configurations[key].value} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value}
|
||||
</div>
|
||||
)}
|
||||
{typeof tool_configurations[key].value === 'number' && (
|
||||
<div title={Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{Number.isNaN(tool_configurations[key].value) ? '' : tool_configurations[key].value}
|
||||
</div>
|
||||
)}
|
||||
{typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && (
|
||||
<div title={tool_configurations[key].model} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'>
|
||||
{tool_configurations[key].model}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
150
dify/web/app/components/workflow/nodes/tool/panel.tsx
Normal file
150
dify/web/app/components/workflow/nodes/tool/panel.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Split from '../_base/components/split'
|
||||
import type { ToolNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import ToolForm from './components/tool-form'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { wrapStructuredVarItem } from '@/app/components/workflow/utils/tool'
|
||||
import useMatchSchemaType, { getMatchedSchemaType } from '../_base/components/variable/use-match-schema-type'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.tool'
|
||||
|
||||
const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
toolInputVarSchema,
|
||||
setInputVar,
|
||||
toolSettingSchema,
|
||||
toolSettingValue,
|
||||
setToolSettingValue,
|
||||
currCollection,
|
||||
isShowAuthBtn,
|
||||
isLoading,
|
||||
outputSchema,
|
||||
hasObjectOutput,
|
||||
currTool,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const [collapsed, setCollapsed] = React.useState(false)
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
const { schemaTypeDefinitions } = useMatchSchemaType()
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='flex h-[200px] items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='pt-2'>
|
||||
{!isShowAuthBtn && (
|
||||
<div className='relative'>
|
||||
{toolInputVarSchema.length > 0 && (
|
||||
<Field
|
||||
className='px-4'
|
||||
title={t(`${i18nPrefix}.inputVars`)}
|
||||
>
|
||||
<ToolForm
|
||||
readOnly={readOnly}
|
||||
nodeId={id}
|
||||
schema={toolInputVarSchema as any}
|
||||
value={inputs.tool_parameters}
|
||||
onChange={setInputVar}
|
||||
currentProvider={currCollection}
|
||||
currentTool={currTool}
|
||||
showManageInputField={!!pipelineId}
|
||||
onManageInputField={() => setShowInputFieldPanel?.(true)}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{toolInputVarSchema.length > 0 && toolSettingSchema.length > 0 && (
|
||||
<Split className='mt-1' />
|
||||
)}
|
||||
|
||||
{toolSettingSchema.length > 0 && (
|
||||
<>
|
||||
<OutputVars
|
||||
title={t(`${i18nPrefix}.settings`)}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
>
|
||||
<ToolForm
|
||||
readOnly={readOnly}
|
||||
nodeId={id}
|
||||
schema={toolSettingSchema as any}
|
||||
value={toolSettingValue}
|
||||
onChange={setToolSettingValue}
|
||||
/>
|
||||
</OutputVars>
|
||||
<Split />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='text'
|
||||
type='string'
|
||||
description={t(`${i18nPrefix}.outputVars.text`)}
|
||||
isIndent={hasObjectOutput}
|
||||
/>
|
||||
<VarItem
|
||||
name='files'
|
||||
type='array[file]'
|
||||
description={t(`${i18nPrefix}.outputVars.files.title`)}
|
||||
isIndent={hasObjectOutput}
|
||||
/>
|
||||
<VarItem
|
||||
name='json'
|
||||
type='array[object]'
|
||||
description={t(`${i18nPrefix}.outputVars.json`)}
|
||||
isIndent={hasObjectOutput}
|
||||
/>
|
||||
{outputSchema.map((outputItem) => {
|
||||
const schemaType = getMatchedSchemaType(outputItem.value, schemaTypeDefinitions)
|
||||
return (
|
||||
<div key={outputItem.name}>
|
||||
{outputItem.value?.type === 'object' ? (
|
||||
<StructureOutputItem
|
||||
rootClassName='code-sm-semibold text-text-secondary'
|
||||
payload={wrapStructuredVarItem(outputItem, schemaType)}
|
||||
/>
|
||||
) : (
|
||||
<VarItem
|
||||
name={outputItem.name}
|
||||
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
||||
type={`${outputItem.type.toLocaleLowerCase()}${schemaType ? ` (${schemaType})` : ''}`}
|
||||
description={outputItem.description}
|
||||
isIndent={hasObjectOutput}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
26
dify/web/app/components/workflow/nodes/tool/types.ts
Normal file
26
dify/web/app/components/workflow/nodes/tool/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Collection, CollectionType } from '@/app/components/tools/types'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import type { ResourceVarInputs } from '../_base/types'
|
||||
|
||||
// Use base types directly
|
||||
export { VarKindType as VarType } from '../_base/types'
|
||||
export type ToolVarInputs = ResourceVarInputs
|
||||
|
||||
export type ToolNodeType = CommonNodeType & {
|
||||
provider_id: string
|
||||
provider_type: CollectionType
|
||||
provider_name: string
|
||||
tool_name: string
|
||||
tool_label: string
|
||||
tool_parameters: ToolVarInputs
|
||||
tool_configurations: Record<string, any>
|
||||
paramSchemas?: Record<string, any>[]
|
||||
version?: string
|
||||
tool_node_version?: string
|
||||
tool_description?: string
|
||||
is_team_authorization?: boolean
|
||||
params?: Record<string, any>
|
||||
plugin_id?: string
|
||||
provider_icon?: Collection['icon']
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
298
dify/web/app/components/workflow/nodes/tool/use-config.ts
Normal file
298
dify/web/app/components/workflow/nodes/tool/use-config.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { produce } from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useWorkflowStore } from '../../store'
|
||||
import type { ToolNodeType, ToolVarInputs } from './types'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { updateBuiltInToolCredential } from '@/service/tools'
|
||||
import {
|
||||
getConfiguredValue,
|
||||
toolParametersToFormSchemas,
|
||||
} from '@/app/components/tools/utils/to-form-schema'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { canFindTool } from '@/utils'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
useInvalidToolsByType,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
const useConfig = (id: string, payload: ToolNodeType) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const language = useLanguage()
|
||||
const { inputs, setInputs: doSetInputs } = useNodeCrud<ToolNodeType>(
|
||||
id,
|
||||
payload,
|
||||
)
|
||||
/*
|
||||
* tool_configurations: tool setting, not dynamic setting (form type = form)
|
||||
* tool_parameters: tool dynamic setting(form type = llm)
|
||||
*/
|
||||
const {
|
||||
provider_id,
|
||||
provider_type,
|
||||
tool_name,
|
||||
tool_configurations,
|
||||
tool_parameters,
|
||||
} = inputs
|
||||
const isBuiltIn = provider_type === CollectionType.builtIn
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
const currentTools = useMemo(() => {
|
||||
switch (provider_type) {
|
||||
case CollectionType.builtIn:
|
||||
return buildInTools || []
|
||||
case CollectionType.custom:
|
||||
return customTools || []
|
||||
case CollectionType.workflow:
|
||||
return workflowTools || []
|
||||
case CollectionType.mcp:
|
||||
return mcpTools || []
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}, [buildInTools, customTools, mcpTools, provider_type, workflowTools])
|
||||
const currCollection = useMemo(() => {
|
||||
return currentTools.find(item => canFindTool(item.id, provider_id))
|
||||
}, [currentTools, provider_id])
|
||||
|
||||
// Auth
|
||||
const needAuth = !!currCollection?.allow_delete
|
||||
const isAuthed = !!currCollection?.is_team_authorization
|
||||
const isShowAuthBtn = isBuiltIn && needAuth && !isAuthed
|
||||
const [
|
||||
showSetAuth,
|
||||
{ setTrue: showSetAuthModal, setFalse: hideSetAuthModal },
|
||||
] = useBoolean(false)
|
||||
|
||||
const invalidToolsByType = useInvalidToolsByType(provider_type)
|
||||
const handleSaveAuth = useCallback(
|
||||
async (value: any) => {
|
||||
await updateBuiltInToolCredential(currCollection?.name as string, value)
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
invalidToolsByType()
|
||||
hideSetAuthModal()
|
||||
},
|
||||
[
|
||||
currCollection?.name,
|
||||
hideSetAuthModal,
|
||||
t,
|
||||
invalidToolsByType,
|
||||
provider_type,
|
||||
],
|
||||
)
|
||||
|
||||
const currTool = useMemo(() => {
|
||||
return currCollection?.tools.find(tool => tool.name === tool_name)
|
||||
}, [currCollection, tool_name])
|
||||
const formSchemas = useMemo(() => {
|
||||
return currTool ? toolParametersToFormSchemas(currTool.parameters) : []
|
||||
}, [currTool])
|
||||
const toolInputVarSchema = useMemo(() => {
|
||||
return formSchemas.filter((item: any) => item.form === 'llm')
|
||||
}, [formSchemas])
|
||||
// use setting
|
||||
const toolSettingSchema = useMemo(() => {
|
||||
return formSchemas.filter((item: any) => item.form !== 'llm')
|
||||
}, [formSchemas])
|
||||
const hasShouldTransferTypeSettingInput = toolSettingSchema.some(
|
||||
item => item.type === 'boolean' || item.type === 'number-input',
|
||||
)
|
||||
|
||||
const setInputs = useCallback(
|
||||
(value: ToolNodeType) => {
|
||||
if (!hasShouldTransferTypeSettingInput) {
|
||||
doSetInputs(value)
|
||||
return
|
||||
}
|
||||
const newInputs = produce(value, (draft) => {
|
||||
const newConfig = { ...draft.tool_configurations }
|
||||
Object.keys(draft.tool_configurations).forEach((key) => {
|
||||
const schema = formSchemas.find(item => item.variable === key)
|
||||
const value = newConfig[key]
|
||||
if (schema?.type === 'boolean') {
|
||||
if (typeof value === 'string')
|
||||
newConfig[key] = value === 'true' || value === '1'
|
||||
|
||||
if (typeof value === 'number') newConfig[key] = value === 1
|
||||
}
|
||||
|
||||
if (schema?.type === 'number-input') {
|
||||
if (typeof value === 'string' && value !== '')
|
||||
newConfig[key] = Number.parseFloat(value)
|
||||
}
|
||||
})
|
||||
draft.tool_configurations = newConfig
|
||||
})
|
||||
doSetInputs(newInputs)
|
||||
},
|
||||
[doSetInputs, formSchemas, hasShouldTransferTypeSettingInput],
|
||||
)
|
||||
const [notSetDefaultValue, setNotSetDefaultValue] = useState(false)
|
||||
const toolSettingValue = useMemo(() => {
|
||||
if (notSetDefaultValue) return tool_configurations
|
||||
return getConfiguredValue(tool_configurations, toolSettingSchema)
|
||||
}, [notSetDefaultValue, toolSettingSchema, tool_configurations])
|
||||
const setToolSettingValue = useCallback(
|
||||
(value: Record<string, any>) => {
|
||||
setNotSetDefaultValue(true)
|
||||
setInputs({
|
||||
...inputs,
|
||||
tool_configurations: value,
|
||||
})
|
||||
},
|
||||
[inputs, setInputs],
|
||||
)
|
||||
|
||||
const formattingParameters = () => {
|
||||
const inputsWithDefaultValue = produce(inputs, (draft) => {
|
||||
if (
|
||||
!draft.tool_configurations
|
||||
|| Object.keys(draft.tool_configurations).length === 0
|
||||
) {
|
||||
draft.tool_configurations = getConfiguredValue(
|
||||
tool_configurations,
|
||||
toolSettingSchema,
|
||||
)
|
||||
}
|
||||
if (
|
||||
!draft.tool_parameters
|
||||
|| Object.keys(draft.tool_parameters).length === 0
|
||||
) {
|
||||
draft.tool_parameters = getConfiguredValue(
|
||||
tool_parameters,
|
||||
toolInputVarSchema,
|
||||
)
|
||||
}
|
||||
})
|
||||
return inputsWithDefaultValue
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!currTool) return
|
||||
const inputsWithDefaultValue = formattingParameters()
|
||||
const { setControlPromptEditorRerenderKey } = workflowStore.getState()
|
||||
setInputs(inputsWithDefaultValue)
|
||||
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
||||
}, [currTool])
|
||||
|
||||
// setting when call
|
||||
const setInputVar = useCallback(
|
||||
(value: ToolVarInputs) => {
|
||||
setInputs({
|
||||
...inputs,
|
||||
tool_parameters: value,
|
||||
})
|
||||
},
|
||||
[inputs, setInputs],
|
||||
)
|
||||
|
||||
const isLoading = currTool && (isBuiltIn ? !currCollection : false)
|
||||
|
||||
const getMoreDataForCheckValid = () => {
|
||||
return {
|
||||
toolInputsSchema: (() => {
|
||||
const formInputs: InputVar[] = []
|
||||
toolInputVarSchema.forEach((item: any) => {
|
||||
formInputs.push({
|
||||
label: item.label[language] || item.label.en_US,
|
||||
variable: item.variable,
|
||||
type: item.type,
|
||||
required: item.required,
|
||||
})
|
||||
})
|
||||
return formInputs
|
||||
})(),
|
||||
notAuthed: isShowAuthBtn,
|
||||
toolSettingSchema,
|
||||
language,
|
||||
}
|
||||
}
|
||||
|
||||
const outputSchema = useMemo(() => {
|
||||
const res: any[] = []
|
||||
const output_schema = currTool?.output_schema
|
||||
if (!output_schema || !output_schema.properties) return res
|
||||
|
||||
Object.keys(output_schema.properties).forEach((outputKey) => {
|
||||
const output = output_schema.properties[outputKey]
|
||||
const type = output.type
|
||||
if (type === 'object') {
|
||||
res.push({
|
||||
name: outputKey,
|
||||
value: output,
|
||||
})
|
||||
}
|
||||
else {
|
||||
res.push({
|
||||
name: outputKey,
|
||||
type:
|
||||
output.type === 'array'
|
||||
? `Array[${output.items?.type
|
||||
? output.items.type.slice(0, 1).toLocaleUpperCase()
|
||||
+ output.items.type.slice(1)
|
||||
: 'Unknown'
|
||||
}]`
|
||||
: `${output.type
|
||||
? output.type.slice(0, 1).toLocaleUpperCase()
|
||||
+ output.type.slice(1)
|
||||
: 'Unknown'
|
||||
}`,
|
||||
description: output.description,
|
||||
})
|
||||
}
|
||||
})
|
||||
return res
|
||||
}, [currTool])
|
||||
|
||||
const hasObjectOutput = useMemo(() => {
|
||||
const output_schema = currTool?.output_schema
|
||||
if (!output_schema || !output_schema.properties) return false
|
||||
const properties = output_schema.properties
|
||||
return Object.keys(properties).some(
|
||||
key => properties[key].type === 'object',
|
||||
)
|
||||
}, [currTool])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
currTool,
|
||||
toolSettingSchema,
|
||||
toolSettingValue,
|
||||
setToolSettingValue,
|
||||
toolInputVarSchema,
|
||||
setInputVar,
|
||||
currCollection,
|
||||
isShowAuthBtn,
|
||||
showSetAuth,
|
||||
showSetAuthModal,
|
||||
hideSetAuthModal,
|
||||
handleSaveAuth,
|
||||
isLoading,
|
||||
outputSchema,
|
||||
hasObjectOutput,
|
||||
getMoreDataForCheckValid,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ToolNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
payload: ToolNodeType,
|
||||
}
|
||||
|
||||
const useGetDataForCheckMore = ({
|
||||
id,
|
||||
payload,
|
||||
}: Params) => {
|
||||
const { getMoreDataForCheckValid } = useConfig(id, payload)
|
||||
|
||||
return {
|
||||
getData: getMoreDataForCheckValid,
|
||||
}
|
||||
}
|
||||
|
||||
export default useGetDataForCheckMore
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { type ToolNodeType, VarType } from './types'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import { produce } from 'immer'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log'
|
||||
import { useToolIcon } from '../../hooks'
|
||||
|
||||
type Params = {
|
||||
id: string,
|
||||
payload: ToolNodeType,
|
||||
runInputData: Record<string, any>
|
||||
runInputDataRef: RefObject<Record<string, any>>
|
||||
getInputVars: (textList: string[]) => InputVar[]
|
||||
setRunInputData: (data: Record<string, any>) => void
|
||||
toVarInputs: (variables: Variable[]) => InputVar[]
|
||||
runResult: NodeTracing
|
||||
}
|
||||
const useSingleRunFormParams = ({
|
||||
id,
|
||||
payload,
|
||||
getInputVars,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
}: Params) => {
|
||||
const { t } = useTranslation()
|
||||
const { inputs } = useNodeCrud<ToolNodeType>(id, payload)
|
||||
|
||||
const hadVarParams = Object.keys(inputs.tool_parameters)
|
||||
.filter(key => inputs.tool_parameters[key].type !== VarType.constant)
|
||||
.map(k => inputs.tool_parameters[k])
|
||||
|
||||
const hadVarSettings = Object.keys(inputs.tool_configurations)
|
||||
.filter(key => typeof inputs.tool_configurations[key] === 'object' && inputs.tool_configurations[key].type && inputs.tool_configurations[key].type !== VarType.constant)
|
||||
.map(k => inputs.tool_configurations[k])
|
||||
|
||||
const varInputs = getInputVars([...hadVarParams, ...hadVarSettings].map((p) => {
|
||||
if (p.type === VarType.variable) {
|
||||
// handle the old wrong value not crash the page
|
||||
if (!(p.value as any).join)
|
||||
return `{{#${p.value}#}}`
|
||||
|
||||
return `{{#${(p.value as ValueSelector).join('.')}#}}`
|
||||
}
|
||||
|
||||
return p.value as string
|
||||
}))
|
||||
const [inputVarValues, doSetInputVarValues] = useState<Record<string, any>>({})
|
||||
const setInputVarValues = useCallback((value: Record<string, any>) => {
|
||||
doSetInputVarValues(value)
|
||||
setRunInputData(value)
|
||||
}, [setRunInputData])
|
||||
|
||||
const inputVarValuesWithConstantValue = useCallback(() => {
|
||||
const res = produce(inputVarValues, (draft) => {
|
||||
Object.keys(inputs.tool_parameters).forEach((key: string) => {
|
||||
const { type, value } = inputs.tool_parameters[key]
|
||||
if (type === VarType.constant && (value === undefined || value === null)) {
|
||||
if(!draft.tool_parameters || !draft.tool_parameters[key])
|
||||
return
|
||||
draft[key] = value
|
||||
}
|
||||
})
|
||||
})
|
||||
return res
|
||||
}, [inputs.tool_parameters, inputVarValues])
|
||||
|
||||
const forms = useMemo(() => {
|
||||
const forms: FormProps[] = [{
|
||||
inputs: varInputs,
|
||||
values: inputVarValuesWithConstantValue(),
|
||||
onChange: setInputVarValues,
|
||||
}]
|
||||
return forms
|
||||
}, [inputVarValuesWithConstantValue, setInputVarValues, varInputs])
|
||||
|
||||
const nodeInfo = useMemo(() => {
|
||||
if (!runResult)
|
||||
return null
|
||||
return formatToTracingNodeList([runResult], t)[0]
|
||||
}, [runResult, t])
|
||||
|
||||
const toolIcon = useToolIcon(payload)
|
||||
|
||||
const getDependentVars = () => {
|
||||
return varInputs.map((item) => {
|
||||
// Guard against null/undefined variable to prevent app crash
|
||||
if (!item.variable || typeof item.variable !== 'string')
|
||||
return []
|
||||
|
||||
return item.variable.slice(1, -1).split('.')
|
||||
}).filter(arr => arr.length > 0)
|
||||
}
|
||||
|
||||
return {
|
||||
forms,
|
||||
nodeInfo,
|
||||
toolIcon,
|
||||
getDependentVars,
|
||||
}
|
||||
}
|
||||
|
||||
export default useSingleRunFormParams
|
||||
5
dify/web/app/components/workflow/nodes/tool/utils.ts
Normal file
5
dify/web/app/components/workflow/nodes/tool/utils.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ToolNodeType } from './types'
|
||||
|
||||
export const checkNodeValid = (_payload: ToolNodeType) => {
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user