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:
parent
444fdb79dc
commit
45deaee762
210 changed files with 9951 additions and 2223 deletions
|
|
@ -0,0 +1,123 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '../../../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodeDataUpdate,
|
||||
useWorkflow,
|
||||
} from '../../../hooks'
|
||||
import type {
|
||||
ValueSelector,
|
||||
Var,
|
||||
VarType,
|
||||
} from '../../../types'
|
||||
import { useVariableAssigner } from '../../variable-assigner/hooks'
|
||||
import { filterVar } from '../../variable-assigner/utils'
|
||||
import AddVariablePopup from './add-variable-popup'
|
||||
import { toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
type AddVariablePopupWithPositionProps = {
|
||||
nodeId: string
|
||||
nodeData: any
|
||||
}
|
||||
const AddVariablePopupWithPosition = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
}: AddVariablePopupWithPositionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const ref = useRef(null)
|
||||
const showAssignVariablePopup = useStore(s => s.showAssignVariablePopup)
|
||||
const setShowAssignVariablePopup = useStore(s => s.setShowAssignVariablePopup)
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleAddVariableInAddVariablePopupWithPosition } = useVariableAssigner()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
|
||||
const outputType = useMemo(() => {
|
||||
if (!showAssignVariablePopup)
|
||||
return ''
|
||||
|
||||
if (showAssignVariablePopup.variableAssignerNodeHandleId === 'target')
|
||||
return showAssignVariablePopup.variableAssignerNodeData.output_type
|
||||
|
||||
const group = showAssignVariablePopup.variableAssignerNodeData.advanced_settings?.groups.find(group => group.groupId === showAssignVariablePopup.variableAssignerNodeHandleId)
|
||||
return group?.output_type || ''
|
||||
}, [showAssignVariablePopup])
|
||||
const availableVars = useMemo(() => {
|
||||
if (!showAssignVariablePopup)
|
||||
return []
|
||||
|
||||
return toNodeAvailableVars({
|
||||
parentNode: showAssignVariablePopup.parentNode,
|
||||
t,
|
||||
beforeNodes: [
|
||||
...getBeforeNodesInSameBranch(showAssignVariablePopup.nodeId),
|
||||
{
|
||||
id: showAssignVariablePopup.nodeId,
|
||||
data: showAssignVariablePopup.nodeData,
|
||||
} as any,
|
||||
],
|
||||
isChatMode,
|
||||
filterVar: filterVar(outputType as VarType),
|
||||
})
|
||||
}, [getBeforeNodesInSameBranch, isChatMode, showAssignVariablePopup, t, outputType])
|
||||
|
||||
useClickAway(() => {
|
||||
if (nodeData._holdAddVariablePopup) {
|
||||
handleNodeDataUpdate({
|
||||
id: nodeId,
|
||||
data: {
|
||||
_holdAddVariablePopup: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
handleNodeDataUpdate({
|
||||
id: nodeId,
|
||||
data: {
|
||||
_showAddVariablePopup: false,
|
||||
},
|
||||
})
|
||||
setShowAssignVariablePopup(undefined)
|
||||
}
|
||||
}, ref)
|
||||
|
||||
const handleAddVariable = useCallback((value: ValueSelector, varDetail: Var) => {
|
||||
if (showAssignVariablePopup) {
|
||||
handleAddVariableInAddVariablePopupWithPosition(
|
||||
showAssignVariablePopup.nodeId,
|
||||
showAssignVariablePopup.variableAssignerNodeId,
|
||||
showAssignVariablePopup.variableAssignerNodeHandleId,
|
||||
value,
|
||||
varDetail,
|
||||
)
|
||||
}
|
||||
}, [showAssignVariablePopup, handleAddVariableInAddVariablePopupWithPosition])
|
||||
|
||||
if (!showAssignVariablePopup)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-10'
|
||||
style={{
|
||||
left: showAssignVariablePopup.x,
|
||||
top: showAssignVariablePopup.y,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<AddVariablePopup
|
||||
availableVars={availableVars}
|
||||
onSelect={handleAddVariable}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddVariablePopupWithPosition)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
export type AddVariablePopupProps = {
|
||||
availableVars: NodeOutPutVar[]
|
||||
onSelect: (value: ValueSelector, item: Var) => void
|
||||
}
|
||||
export const AddVariablePopup = ({
|
||||
availableVars,
|
||||
onSelect,
|
||||
}: AddVariablePopupProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='w-[240px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg'>
|
||||
<div className='flex items-center px-4 h-[34px] text-[13px] font-semibold text-gray-700 border-b-[0.5px] border-b-gray-200'>
|
||||
{t('workflow.nodes.variableAssigner.setAssignVariable')}
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
<VarReferenceVars
|
||||
hideSearch
|
||||
vars={availableVars}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddVariablePopup)
|
||||
|
|
@ -7,6 +7,7 @@ import type { InputVar } from '../../../../types'
|
|||
import { BlockEnum, InputVarType } from '../../../../types'
|
||||
import CodeEditor from '../editor/code-editor'
|
||||
import { CodeLanguage } from '../../../code/types'
|
||||
import TextEditor from '../editor/text-editor'
|
||||
import Select from '@/app/components/base/select'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import { Resolution } from '@/types/app'
|
||||
|
|
@ -34,7 +35,7 @@ const FormItem: FC<Props> = ({
|
|||
const { t } = useTranslation()
|
||||
const { type } = payload
|
||||
const fileSettings = useFeatures(s => s.features.file)
|
||||
const handleContextItemChange = useCallback((index: number) => {
|
||||
const handleArrayItemChange = useCallback((index: number) => {
|
||||
return (newValue: any) => {
|
||||
const newValues = produce(value, (draft: any) => {
|
||||
draft[index] = newValue
|
||||
|
|
@ -43,7 +44,7 @@ const FormItem: FC<Props> = ({
|
|||
}
|
||||
}, [value, onChange])
|
||||
|
||||
const handleContextItemRemove = useCallback((index: number) => {
|
||||
const handleArrayItemRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newValues = produce(value, (draft: any) => {
|
||||
draft.splice(index, 1)
|
||||
|
|
@ -77,9 +78,13 @@ const FormItem: FC<Props> = ({
|
|||
}
|
||||
return ''
|
||||
})()
|
||||
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type)
|
||||
const isContext = type === InputVarType.contexts
|
||||
const isIterator = type === InputVarType.iterator
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
{type !== InputVarType.contexts && <div className='h-8 leading-8 text-[13px] font-medium text-gray-700 truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>}
|
||||
{!isArrayLikeType && <div className='h-8 leading-8 text-[13px] font-medium text-gray-700 truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>}
|
||||
<div className='grow'>
|
||||
{
|
||||
type === InputVarType.textInput && (
|
||||
|
|
@ -160,7 +165,7 @@ const FormItem: FC<Props> = ({
|
|||
}
|
||||
|
||||
{
|
||||
type === InputVarType.contexts && (
|
||||
isContext && (
|
||||
<div className='space-y-2'>
|
||||
{(value || []).map((item: any, index: number) => (
|
||||
<CodeEditor
|
||||
|
|
@ -170,13 +175,37 @@ const FormItem: FC<Props> = ({
|
|||
headerRight={
|
||||
(value as any).length > 1
|
||||
? (<Trash03
|
||||
onClick={handleContextItemRemove(index)}
|
||||
onClick={handleArrayItemRemove(index)}
|
||||
className='mr-1 w-3.5 h-3.5 text-gray-500 cursor-pointer'
|
||||
/>)
|
||||
: undefined
|
||||
}
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleContextItemChange(index)}
|
||||
onChange={handleArrayItemChange(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
isIterator && (
|
||||
<div className='space-y-2'>
|
||||
{(value || []).map((item: any, index: number) => (
|
||||
<TextEditor
|
||||
key={index}
|
||||
isInNode
|
||||
value={item}
|
||||
title={<span>{t('appDebug.variableConig.content')} {index + 1} </span>}
|
||||
onChange={handleArrayItemChange(index)}
|
||||
headerRight={
|
||||
(value as any).length > 1
|
||||
? (<Trash03
|
||||
onClick={handleArrayItemRemove(index)}
|
||||
className='mr-1 w-3.5 h-3.5 text-gray-500 cursor-pointer'
|
||||
/>)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,20 +32,22 @@ const Form: FC<Props> = ({
|
|||
onChange(newValues)
|
||||
}
|
||||
}, [values, onChange])
|
||||
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(inputs[0]?.type)
|
||||
const isContext = inputs[0]?.type === InputVarType.contexts
|
||||
const handleAddContext = useCallback(() => {
|
||||
const newValues = produce(values, (draft: any) => {
|
||||
const key = inputs[0].variable
|
||||
draft[key].push(RETRIEVAL_OUTPUT_STRUCT)
|
||||
draft[key].push(isContext ? RETRIEVAL_OUTPUT_STRUCT : '')
|
||||
})
|
||||
onChange(newValues)
|
||||
}, [values, onChange, inputs])
|
||||
}, [values, onChange, inputs, isContext])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'space-y-2')}>
|
||||
{label && (
|
||||
<div className='mb-1 flex items-center justify-between'>
|
||||
<div className='flex items-center h-6 text-xs font-medium text-gray-500 uppercase'>{label}</div>
|
||||
{inputs[0]?.type === InputVarType.contexts && (
|
||||
{isArrayLikeType && (
|
||||
<AddButton onClick={handleAddContext} />
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ const Base: FC<Props> = ({
|
|||
const handleCopy = useCallback(() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, 2000)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@ import type { FC } from 'react'
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { DefaultTFuncReturn } from 'i18next'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
className?: string
|
||||
title: JSX.Element | string | DefaultTFuncReturn
|
||||
tooltip?: string
|
||||
supportFold?: boolean
|
||||
children?: JSX.Element | string | null
|
||||
|
|
@ -16,6 +19,7 @@ type Props = {
|
|||
}
|
||||
|
||||
const Filed: FC<Props> = ({
|
||||
className,
|
||||
title,
|
||||
tooltip,
|
||||
children,
|
||||
|
|
@ -27,7 +31,7 @@ const Filed: FC<Props> = ({
|
|||
toggle: toggleFold,
|
||||
}] = useBoolean(true)
|
||||
return (
|
||||
<div className={cn(inline && 'flex justify-between items-center', supportFold && 'cursor-pointer')}>
|
||||
<div className={cn(className, inline && 'flex justify-between items-center', supportFold && 'cursor-pointer')}>
|
||||
<div
|
||||
onClick={() => supportFold && toggleFold()}
|
||||
className='flex justify-between items-center'>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ListNoDataPlaceholder: FC<Props> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex rounded-md bg-gray-50 items-center min-h-[42px] justify-center leading-[18px] text-xs font-normal text-gray-500'>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ListNoDataPlaceholder)
|
||||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useToolIcon,
|
||||
|
|
@ -33,10 +33,12 @@ const Item = ({
|
|||
const { t } = useTranslation()
|
||||
const { handleNodeChange } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const toolIcon = useToolIcon(data)
|
||||
const availablePrevNodes = nodesExtraData[data.type].availablePrevNodes
|
||||
const availableNextNodes = nodesExtraData[data.type].availableNextNodes
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(data.type, data.isInIteration)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
}, [nodeId, sourceHandle, handleNodeChange])
|
||||
|
|
@ -84,7 +86,7 @@ const Item = ({
|
|||
}}
|
||||
trigger={renderTrigger}
|
||||
popupClassName='!w-[328px]'
|
||||
availableBlocksTypes={intersection(availablePrevNodes, availableNextNodes).filter(item => item !== data.type)}
|
||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== data.type)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import type { Node } from '../../../types'
|
|||
import BlockSelector from '../../../block-selector'
|
||||
import type { ToolDefaultValue } from '../../../block-selector/types'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '../../../hooks'
|
||||
|
|
@ -35,11 +35,12 @@ export const NodeTargetHandle = memo(({
|
|||
}: NodeHandleProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const connected = data._connectedTargetHandleIds?.includes(handleId)
|
||||
const availablePrevNodes = nodesExtraData[data.type].availablePrevNodes
|
||||
const isConnectable = !!availablePrevNodes.length
|
||||
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration)
|
||||
const isConnectable = !!availablePrevBlocks.length && (
|
||||
!data.isIterationStart
|
||||
)
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
|
|
@ -80,7 +81,7 @@ export const NodeTargetHandle = memo(({
|
|||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
!connected && isConnectable && !data._isInvalidConnection && !getNodesReadOnly() && (
|
||||
!connected && isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
|
|
@ -94,7 +95,7 @@ export const NodeTargetHandle = memo(({
|
|||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
availableBlocksTypes={availablePrevNodes}
|
||||
availableBlocksTypes={availablePrevBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -112,12 +113,14 @@ export const NodeSourceHandle = memo(({
|
|||
nodeSelectorClassName,
|
||||
}: NodeHandleProps) => {
|
||||
const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
|
||||
const connectingNodePayload = useStore(s => s.connectingNodePayload)
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const availableNextNodes = nodesExtraData[data.type].availableNextNodes
|
||||
const isConnectable = !!availableNextNodes.length
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration)
|
||||
const isUnConnectable = !availableNextBlocks.length || ((connectingNodePayload?.nodeType === BlockEnum.VariableAssigner || connectingNodePayload?.nodeType === BlockEnum.VariableAggregator) && connectingNodePayload?.handleType === 'target')
|
||||
const isConnectable = !isUnConnectable
|
||||
|
||||
const connected = data._connectedSourceHandleIds?.includes(handleId)
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
|
|
@ -162,7 +165,7 @@ export const NodeSourceHandle = memo(({
|
|||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
!connected && isConnectable && !data._isInvalidConnection && !getNodesReadOnly() && (
|
||||
!connected && isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
|
|
@ -175,7 +178,7 @@ export const NodeSourceHandle = memo(({
|
|||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
availableBlocksTypes={availableNextNodes}
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import type { OnResize } from 'reactflow'
|
||||
import { NodeResizeControl } from 'reactflow'
|
||||
import { useNodesInteractions } from '../../../hooks'
|
||||
import type { CommonNodeType } from '../../../types'
|
||||
|
||||
const Icon = () => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M5.19009 11.8398C8.26416 10.6196 10.7144 8.16562 11.9297 5.08904" stroke="black" strokeOpacity="0.16" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
type NodeResizerProps = {
|
||||
nodeId: string
|
||||
nodeData: CommonNodeType
|
||||
}
|
||||
const NodeResizer = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
}: NodeResizerProps) => {
|
||||
const { handleNodeResize } = useNodesInteractions()
|
||||
|
||||
const handleResize = useCallback<OnResize>((_, params) => {
|
||||
handleNodeResize(nodeId, params)
|
||||
}, [nodeId, handleNodeResize])
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'hidden group-hover:block',
|
||||
nodeData.selected && '!block',
|
||||
)}>
|
||||
<NodeResizeControl
|
||||
position='bottom-right'
|
||||
className='!border-none !bg-transparent'
|
||||
onResize={handleResize}
|
||||
minWidth={272}
|
||||
minHeight={176}
|
||||
>
|
||||
<div className='absolute bottom-[1px] right-[1px]'><Icon /></div>
|
||||
</NodeResizeControl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeResizer)
|
||||
|
|
@ -7,38 +7,39 @@ import { useTranslation } from 'react-i18next'
|
|||
import { intersection } from 'lodash-es'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type ChangeBlockProps = {
|
||||
nodeId: string
|
||||
nodeType: BlockEnum
|
||||
nodeData: Node['data']
|
||||
sourceHandle: string
|
||||
}
|
||||
const ChangeBlock = ({
|
||||
nodeId,
|
||||
nodeType,
|
||||
nodeData,
|
||||
sourceHandle,
|
||||
}: ChangeBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleNodeChange } = useNodesInteractions()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const availablePrevNodes = nodesExtraData[nodeType].availablePrevNodes
|
||||
const availableNextNodes = nodesExtraData[nodeType].availableNextNodes
|
||||
const {
|
||||
availablePrevBlocks,
|
||||
availableNextBlocks,
|
||||
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration)
|
||||
|
||||
const availableNodes = useMemo(() => {
|
||||
if (availableNextNodes.length && availableNextNodes.length)
|
||||
return intersection(availablePrevNodes, availableNextNodes)
|
||||
else if (availablePrevNodes.length)
|
||||
return availablePrevNodes
|
||||
if (availablePrevBlocks.length && availableNextBlocks.length)
|
||||
return intersection(availablePrevBlocks, availableNextBlocks)
|
||||
else if (availablePrevBlocks.length)
|
||||
return availablePrevBlocks
|
||||
else
|
||||
return availableNextNodes
|
||||
}, [availablePrevNodes, availableNextNodes])
|
||||
return availableNextBlocks
|
||||
}, [availablePrevBlocks, availableNextBlocks])
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
|||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
|
||||
type PanelOperatorPopupProps = {
|
||||
id: string
|
||||
|
|
@ -46,28 +47,35 @@ const PanelOperatorPopup = ({
|
|||
const nodesExtraData = useNodesExtraData()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const edge = edges.find(edge => edge.target === id)
|
||||
const author = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].author
|
||||
|
||||
if (data.provider_type === 'builtin')
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
|
||||
if (data.provider_type === CollectionType.workflow)
|
||||
return workflowTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
}, [data, nodesExtraData, buildInTools, customTools])
|
||||
}, [data, nodesExtraData, buildInTools, customTools, workflowTools])
|
||||
|
||||
const about = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].about
|
||||
|
||||
if (data.provider_type === 'builtin')
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
}, [data, nodesExtraData, language, buildInTools, customTools])
|
||||
if (data.provider_type === CollectionType.workflow)
|
||||
return workflowTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
|
||||
const showChangeBlock = data.type !== BlockEnum.Start && !nodesReadOnly
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
}, [data, nodesExtraData, language, buildInTools, customTools, workflowTools])
|
||||
|
||||
const showChangeBlock = data.type !== BlockEnum.Start && !nodesReadOnly && data.type !== BlockEnum.Iteration
|
||||
|
||||
return (
|
||||
<div className='w-[240px] border-[0.5px] border-gray-200 rounded-lg shadow-xl bg-white'>
|
||||
|
|
@ -97,7 +105,7 @@ const PanelOperatorPopup = ({
|
|||
showChangeBlock && (
|
||||
<ChangeBlock
|
||||
nodeId={id}
|
||||
nodeType={data.type}
|
||||
nodeData={data}
|
||||
sourceHandle={edge?.sourceHandle || 'source'}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
|
|||
nodeId,
|
||||
value,
|
||||
}) => {
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = getBeforeNodesInSameBranch(nodeId)
|
||||
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const availableNodes = getBeforeNodesInSameBranchIncludeParent(nodeId)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import type { QuestionClassifierNodeType } from '../../../question-classifier/ty
|
|||
import type { HttpNodeType } from '../../../http/types'
|
||||
import { VarType as ToolVarType } from '../../../tool/types'
|
||||
import type { ToolNodeType } from '../../../tool/types'
|
||||
import type { ParameterExtractorNodeType } from '../../../parameter-extractor/types'
|
||||
import type { IterationNodeType } from '../../../iteration/types'
|
||||
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
|
|
@ -19,6 +21,7 @@ import {
|
|||
HTTP_REQUEST_OUTPUT_STRUCT,
|
||||
KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT,
|
||||
LLM_OUTPUT_STRUCT,
|
||||
PARAMETER_EXTRACTOR_COMMON_STRUCT,
|
||||
QUESTION_CLASSIFIER_OUTPUT_STRUCT,
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
TEMPLATE_TRANSFORM_OUTPUT_STRUCT,
|
||||
|
|
@ -27,6 +30,10 @@ import {
|
|||
import type { PromptItem } from '@/models/debug'
|
||||
import { VAR_REGEX } from '@/config'
|
||||
|
||||
export const isSystemVar = (valueSelector: ValueSelector) => {
|
||||
return valueSelector[0] === 'sys' || valueSelector[1] === 'sys'
|
||||
}
|
||||
|
||||
const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
if (type === InputVarType.number)
|
||||
return VarType.number
|
||||
|
|
@ -54,6 +61,7 @@ const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: Val
|
|||
|
||||
const formatItem = (item: any, isChatMode: boolean, filterVar: (payload: Var, selector: ValueSelector) => boolean): NodeOutPutVar => {
|
||||
const { id, data } = item
|
||||
|
||||
const res: NodeOutPutVar = {
|
||||
nodeId: id,
|
||||
title: data.title,
|
||||
|
|
@ -136,13 +144,58 @@ const formatItem = (item: any, isChatMode: boolean, filterVar: (payload: Var, se
|
|||
case BlockEnum.VariableAssigner: {
|
||||
const {
|
||||
output_type,
|
||||
advanced_settings,
|
||||
} = data as VariableAssignerNodeType
|
||||
res.vars = [
|
||||
{
|
||||
variable: 'output',
|
||||
type: output_type,
|
||||
},
|
||||
]
|
||||
const isGroup = !!advanced_settings?.group_enabled
|
||||
if (!isGroup) {
|
||||
res.vars = [
|
||||
{
|
||||
variable: 'output',
|
||||
type: output_type,
|
||||
},
|
||||
]
|
||||
}
|
||||
else {
|
||||
res.vars = advanced_settings?.groups.map((group) => {
|
||||
return {
|
||||
variable: group.group_name,
|
||||
type: VarType.object,
|
||||
children: [{
|
||||
variable: 'output',
|
||||
type: group.output_type,
|
||||
}],
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.VariableAggregator: {
|
||||
const {
|
||||
output_type,
|
||||
advanced_settings,
|
||||
} = data as VariableAssignerNodeType
|
||||
const isGroup = !!advanced_settings?.group_enabled
|
||||
if (!isGroup) {
|
||||
res.vars = [
|
||||
{
|
||||
variable: 'output',
|
||||
type: output_type,
|
||||
},
|
||||
]
|
||||
}
|
||||
else {
|
||||
res.vars = advanced_settings?.groups.map((group) => {
|
||||
return {
|
||||
variable: group.group_name,
|
||||
type: VarType.object,
|
||||
children: [{
|
||||
variable: 'output',
|
||||
type: group.output_type,
|
||||
}],
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +203,28 @@ const formatItem = (item: any, isChatMode: boolean, filterVar: (payload: Var, se
|
|||
res.vars = TOOL_OUTPUT_STRUCT
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.ParameterExtractor: {
|
||||
res.vars = [
|
||||
...PARAMETER_EXTRACTOR_COMMON_STRUCT,
|
||||
...((data as ParameterExtractorNodeType).parameters || []).map((p) => {
|
||||
return {
|
||||
variable: p.name,
|
||||
type: p.type as unknown as VarType,
|
||||
}
|
||||
})]
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Iteration: {
|
||||
res.vars = [
|
||||
{
|
||||
variable: 'output',
|
||||
type: (data as IterationNodeType).output_type || VarType.arrayString,
|
||||
},
|
||||
]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const selector = [id]
|
||||
|
|
@ -183,37 +258,113 @@ export const toNodeOutputVars = (nodes: any[], isChatMode: boolean, filterVar =
|
|||
return res
|
||||
}
|
||||
|
||||
export const isSystemVar = (valueSelector: ValueSelector) => {
|
||||
return valueSelector[0] === 'sys' || valueSelector[1] === 'sys'
|
||||
const getIterationItemType = ({
|
||||
valueSelector,
|
||||
beforeNodesOutputVars,
|
||||
}: {
|
||||
valueSelector: ValueSelector
|
||||
beforeNodesOutputVars: NodeOutPutVar[]
|
||||
}): VarType => {
|
||||
const outputVarNodeId = valueSelector[0]
|
||||
const targetVar = beforeNodesOutputVars.find(v => v.nodeId === outputVarNodeId)
|
||||
if (!targetVar)
|
||||
return VarType.string
|
||||
|
||||
let arrayType: VarType = VarType.string
|
||||
|
||||
const isSystem = isSystemVar(valueSelector)
|
||||
let curr: any = targetVar.vars
|
||||
|
||||
if (isSystem)
|
||||
return curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type;
|
||||
|
||||
(valueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
curr = curr?.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
arrayType = curr?.type
|
||||
}
|
||||
else {
|
||||
if (curr?.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
switch (arrayType as VarType) {
|
||||
case VarType.arrayString:
|
||||
return VarType.string
|
||||
case VarType.arrayNumber:
|
||||
return VarType.number
|
||||
case VarType.arrayObject:
|
||||
return VarType.object
|
||||
case VarType.array:
|
||||
return VarType.any
|
||||
case VarType.arrayFile:
|
||||
return VarType.object
|
||||
default:
|
||||
return VarType.string
|
||||
}
|
||||
}
|
||||
|
||||
export const getNodeInfoById = (nodes: any, id: string) => {
|
||||
if (!isArray(nodes))
|
||||
return
|
||||
return nodes.find((node: any) => node.id === id)
|
||||
}
|
||||
export const getVarType = ({
|
||||
parentNode,
|
||||
valueSelector,
|
||||
isIterationItem,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant,
|
||||
}:
|
||||
{
|
||||
valueSelector: ValueSelector
|
||||
parentNode?: Node | null
|
||||
isIterationItem?: boolean
|
||||
availableNodes: any[]
|
||||
isChatMode: boolean
|
||||
isConstant?: boolean
|
||||
}): VarType => {
|
||||
if (isConstant)
|
||||
return VarType.string
|
||||
|
||||
export const getVarType = (value: ValueSelector, availableNodes: any[], isChatMode: boolean): VarType | undefined => {
|
||||
const isSystem = isSystemVar(value)
|
||||
const beforeNodesOutputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
|
||||
const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration
|
||||
if (isIterationItem) {
|
||||
return getIterationItemType({
|
||||
valueSelector,
|
||||
beforeNodesOutputVars,
|
||||
})
|
||||
}
|
||||
if (isIterationInnerVar) {
|
||||
if (valueSelector[1] === 'item') {
|
||||
const itemType = getIterationItemType({
|
||||
valueSelector: (parentNode?.data as any).iterator_selector || [],
|
||||
beforeNodesOutputVars,
|
||||
})
|
||||
return itemType
|
||||
}
|
||||
if (valueSelector[1] === 'index')
|
||||
return VarType.number
|
||||
|
||||
return VarType.string
|
||||
}
|
||||
const isSystem = isSystemVar(valueSelector)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
|
||||
const targetVarNodeId = isSystem ? startNode?.id : value[0]
|
||||
const targetVar = allOutputVars.find(v => v.nodeId === targetVarNodeId)
|
||||
const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0]
|
||||
const targetVar = beforeNodesOutputVars.find(v => v.nodeId === targetVarNodeId)
|
||||
|
||||
if (!targetVar)
|
||||
return undefined
|
||||
return VarType.string
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
if (isSystem) {
|
||||
return curr.find((v: any) => v.variable === (value as ValueSelector).join('.'))?.type
|
||||
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
|
||||
}
|
||||
else {
|
||||
(value as ValueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === value.length - 2
|
||||
(valueSelector as ValueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
curr = curr.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
type = curr?.type
|
||||
|
|
@ -227,6 +378,57 @@ export const getVarType = (value: ValueSelector, availableNodes: any[], isChatMo
|
|||
}
|
||||
}
|
||||
|
||||
// node output vars + parent inner vars(if in iteration or other wrap node)
|
||||
export const toNodeAvailableVars = ({
|
||||
parentNode,
|
||||
t,
|
||||
beforeNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
}: {
|
||||
parentNode?: Node | null
|
||||
t?: any
|
||||
// to get those nodes output vars
|
||||
beforeNodes: Node[]
|
||||
isChatMode: boolean
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
}): NodeOutPutVar[] => {
|
||||
const beforeNodesOutputVars = toNodeOutputVars(beforeNodes, isChatMode, filterVar)
|
||||
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
||||
if (isInIteration) {
|
||||
const iterationNode: any = parentNode
|
||||
const itemType = getVarType({
|
||||
parentNode: iterationNode,
|
||||
isIterationItem: true,
|
||||
valueSelector: iterationNode?.data.iterator_selector || [],
|
||||
availableNodes: beforeNodes,
|
||||
isChatMode,
|
||||
})
|
||||
const iterationVar = {
|
||||
nodeId: iterationNode?.id,
|
||||
title: t('workflow.nodes.iteration.currentIteration'),
|
||||
vars: [
|
||||
{
|
||||
variable: 'item',
|
||||
type: itemType,
|
||||
},
|
||||
{
|
||||
variable: 'index',
|
||||
type: VarType.number,
|
||||
},
|
||||
],
|
||||
}
|
||||
beforeNodesOutputVars.unshift(iterationVar)
|
||||
}
|
||||
return beforeNodesOutputVars
|
||||
}
|
||||
|
||||
export const getNodeInfoById = (nodes: any, id: string) => {
|
||||
if (!isArray(nodes))
|
||||
return
|
||||
return nodes.find((node: any) => node.id === id)
|
||||
}
|
||||
|
||||
const matchNotSystemVars = (prompts: string[]) => {
|
||||
if (!prompts)
|
||||
return []
|
||||
|
|
@ -326,11 +528,98 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
|||
|
||||
case BlockEnum.VariableAssigner: {
|
||||
res = (data as VariableAssignerNodeType)?.variables
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.VariableAggregator: {
|
||||
res = (data as VariableAssignerNodeType)?.variables
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.ParameterExtractor: {
|
||||
const payload = (data as ParameterExtractorNodeType)
|
||||
res = [payload.query]
|
||||
const varInInstructions = matchNotSystemVars([payload.instruction || ''])
|
||||
res.push(...varInInstructions)
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Iteration: {
|
||||
res = [(data as IterationNodeType).iterator_selector]
|
||||
break
|
||||
}
|
||||
}
|
||||
return res || []
|
||||
}
|
||||
|
||||
// used can be used in iteration node
|
||||
export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSelector): string | string[] => {
|
||||
const { data } = node
|
||||
const { type } = data
|
||||
let res: string | string[] = ''
|
||||
switch (type) {
|
||||
case BlockEnum.LLM: {
|
||||
const payload = (data as LLMNodeType)
|
||||
res = [`#${valueSelector.join('.')}#`]
|
||||
if (payload.context?.variable_selector.join('.') === valueSelector.join('.'))
|
||||
res.push('#context#')
|
||||
|
||||
break
|
||||
}
|
||||
case BlockEnum.KnowledgeRetrieval: {
|
||||
res = 'query'
|
||||
break
|
||||
}
|
||||
case BlockEnum.IfElse: {
|
||||
const targetVar = (data as IfElseNodeType).conditions?.find(c => c.variable_selector.join('.') === valueSelector.join('.'))
|
||||
if (targetVar)
|
||||
res = `#${valueSelector.join('.')}#`
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
const targetVar = (data as CodeNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.'))
|
||||
if (targetVar)
|
||||
res = targetVar.variable
|
||||
break
|
||||
}
|
||||
case BlockEnum.TemplateTransform: {
|
||||
const targetVar = (data as TemplateTransformNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.'))
|
||||
if (targetVar)
|
||||
res = targetVar.variable
|
||||
break
|
||||
}
|
||||
case BlockEnum.QuestionClassifier: {
|
||||
res = 'query'
|
||||
break
|
||||
}
|
||||
case BlockEnum.HttpRequest: {
|
||||
res = `#${valueSelector.join('.')}#`
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Tool: {
|
||||
res = `#${valueSelector.join('.')}#`
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.VariableAssigner: {
|
||||
res = `#${valueSelector.join('.')}#`
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.VariableAggregator: {
|
||||
res = `#${valueSelector.join('.')}#`
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.ParameterExtractor: {
|
||||
res = 'query'
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export const findUsedVarNodes = (varSelector: ValueSelector, availableNodes: Node[]): Node[] => {
|
||||
const res: Node[] = []
|
||||
availableNodes.forEach((node) => {
|
||||
|
|
@ -345,6 +634,7 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
|||
const newNode = produce(oldNode, (draft: any) => {
|
||||
const { data } = draft
|
||||
const { type } = data
|
||||
|
||||
switch (type) {
|
||||
case BlockEnum.End: {
|
||||
const payload = data as EndNodeType
|
||||
|
|
@ -480,6 +770,31 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
|||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.VariableAggregator: {
|
||||
const payload = data as VariableAssignerNodeType
|
||||
if (payload.variables) {
|
||||
payload.variables = payload.variables.map((v) => {
|
||||
if (v.join('.') === oldVarSelector.join('.'))
|
||||
v = newVarSelector
|
||||
return v
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.ParameterExtractor: {
|
||||
const payload = data as ParameterExtractorNodeType
|
||||
if (payload.query.join('.') === oldVarSelector.join('.'))
|
||||
payload.query = newVarSelector
|
||||
payload.instruction = replaceOldVarInText(payload.instruction, oldVarSelector, newVarSelector)
|
||||
break
|
||||
}
|
||||
case BlockEnum.Iteration: {
|
||||
const payload = data as IterationNodeType
|
||||
if (payload.iterator_selector.join('.') === oldVarSelector.join('.'))
|
||||
payload.iterator_selector = newVarSelector
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
return newNode
|
||||
|
|
@ -567,10 +882,33 @@ export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelecto
|
|||
break
|
||||
}
|
||||
|
||||
case BlockEnum.VariableAggregator: {
|
||||
res.push([id, 'output'])
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Tool: {
|
||||
varsToValueSelectorList(TOOL_OUTPUT_STRUCT, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.ParameterExtractor: {
|
||||
const {
|
||||
parameters,
|
||||
} = data as ParameterExtractorNodeType
|
||||
if (parameters?.length > 0) {
|
||||
parameters.forEach((p) => {
|
||||
res.push([id, p.name])
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Iteration: {
|
||||
res.push([id, 'output'])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import VarReferencePopup from './var-reference-popup'
|
||||
import { getNodeInfoById, isSystemVar, toNodeOutputVars } from './utils'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import { getNodeInfoById, getVarType, isSystemVar, toNodeAvailableVars } from './utils'
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
|
|
@ -24,7 +25,7 @@ import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/typ
|
|||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
const TRIGGER_DEFAULT_WIDTH = 227
|
||||
|
||||
type Props = {
|
||||
|
|
@ -33,12 +34,15 @@ type Props = {
|
|||
isShowNodeName: boolean
|
||||
readonly: boolean
|
||||
value: ValueSelector | string
|
||||
onChange: (value: ValueSelector | string, varKindType: VarKindType) => void
|
||||
onChange: (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => void
|
||||
onOpen?: () => void
|
||||
isSupportConstantValue?: boolean
|
||||
defaultVarKindType?: VarKindType
|
||||
onlyLeafNodeVar?: boolean
|
||||
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
|
||||
availableNodes?: Node[]
|
||||
availableVars?: NodeOutPutVar[]
|
||||
isAddBtnTrigger?: boolean
|
||||
}
|
||||
|
||||
const VarReferencePicker: FC<Props> = ({
|
||||
|
|
@ -53,8 +57,27 @@ const VarReferencePicker: FC<Props> = ({
|
|||
defaultVarKindType = VarKindType.constant,
|
||||
onlyLeafNodeVar,
|
||||
filterVar = () => true,
|
||||
availableNodes: passedInAvailableNodes,
|
||||
availableVars,
|
||||
isAddBtnTrigger,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const node = getNodes().find(n => n.id === nodeId)
|
||||
const isInIteration = !!node?.data.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === node.parentId) : null
|
||||
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const [triggerWidth, setTriggerWidth] = useState(TRIGGER_DEFAULT_WIDTH)
|
||||
useEffect(() => {
|
||||
|
|
@ -63,63 +86,60 @@ const VarReferencePicker: FC<Props> = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [triggerRef.current])
|
||||
|
||||
const isChatMode = useIsChatMode()
|
||||
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
|
||||
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)
|
||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
const outputVars = toNodeOutputVars(availableNodes, isChatMode, filterVar)
|
||||
|
||||
const outputVars = (() => {
|
||||
if (availableVars)
|
||||
return availableVars
|
||||
|
||||
const vars = toNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
t,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
})
|
||||
|
||||
return vars
|
||||
})()
|
||||
const [open, setOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
const hasValue = !isConstant && value.length > 0
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const isIterationVar = (() => {
|
||||
if (!isInIteration)
|
||||
return false
|
||||
if (value[0] === node?.parentId && ['item', 'index'].includes(value[1]))
|
||||
return true
|
||||
return false
|
||||
})()
|
||||
|
||||
const outputVarNodeId = hasValue ? value[0] : ''
|
||||
const outputVarNode = (() => {
|
||||
if (!hasValue || isConstant)
|
||||
return null
|
||||
|
||||
if (isIterationVar)
|
||||
return iterationNode?.data
|
||||
|
||||
if (isSystemVar(value as ValueSelector))
|
||||
return startNode?.data
|
||||
|
||||
return getNodeInfoById(availableNodes, outputVarNodeId)?.data
|
||||
})()
|
||||
const varName = hasValue ? `${isSystemVar(value as ValueSelector) ? 'sys.' : ''}${value[value.length - 1]}` : ''
|
||||
|
||||
const getVarType = () => {
|
||||
if (isConstant)
|
||||
return 'undefined'
|
||||
const isSystem = isSystemVar(value as ValueSelector)
|
||||
const targetVarNodeId = isSystem ? startNode?.id : outputVarNodeId
|
||||
const targetVar = allOutputVars.find(v => v.nodeId === targetVarNodeId)
|
||||
|
||||
if (!targetVar)
|
||||
return 'undefined'
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
if (isSystem) {
|
||||
return curr.find((v: any) => v.variable === (value as ValueSelector).join('.'))?.type
|
||||
const varName = (() => {
|
||||
if (hasValue) {
|
||||
const isSystem = isSystemVar(value as ValueSelector)
|
||||
const varName = value.length >= 3 ? (value as ValueSelector).slice(-2).join('.') : value[value.length - 1]
|
||||
return `${isSystem ? 'sys.' : ''}${varName}`
|
||||
}
|
||||
else {
|
||||
(value as ValueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === value.length - 2
|
||||
curr = curr.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
type = curr?.type
|
||||
}
|
||||
else {
|
||||
if (curr.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
return type
|
||||
}
|
||||
}
|
||||
return ''
|
||||
})()
|
||||
|
||||
const varKindTypes = [
|
||||
{
|
||||
|
|
@ -150,7 +170,7 @@ const VarReferencePicker: FC<Props> = ({
|
|||
}
|
||||
}, [controlFocus])
|
||||
|
||||
const handleVarReferenceChange = useCallback((value: ValueSelector) => {
|
||||
const handleVarReferenceChange = useCallback((value: ValueSelector, varInfo: Var) => {
|
||||
// sys var not passed to backend
|
||||
const newValue = produce(value, (draft) => {
|
||||
if (draft[1] && draft[1].startsWith('sys')) {
|
||||
|
|
@ -161,7 +181,7 @@ const VarReferencePicker: FC<Props> = ({
|
|||
})
|
||||
}
|
||||
})
|
||||
onChange(newValue, varKindType)
|
||||
onChange(newValue, varKindType, varInfo)
|
||||
setOpen(false)
|
||||
}, [onChange, varKindType])
|
||||
|
||||
|
|
@ -176,7 +196,14 @@ const VarReferencePicker: FC<Props> = ({
|
|||
onChange([], varKindType)
|
||||
}, [onChange, varKindType])
|
||||
|
||||
const type = getVarType()
|
||||
const type = getVarType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector: value as ValueSelector,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: !!isConstant,
|
||||
})
|
||||
|
||||
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
|
||||
const availableWidth = triggerWidth - 56
|
||||
const [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth] = (() => {
|
||||
|
|
@ -193,86 +220,92 @@ const VarReferencePicker: FC<Props> = ({
|
|||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
placement={isAddBtnTrigger ? 'bottom-end' : 'bottom-start'}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
}} className='!flex'>
|
||||
<div ref={triggerRef} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8 p-1 rounded-lg bg-gray-100 border')}>
|
||||
{isSupportConstantValue
|
||||
? <div onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOpen(false)
|
||||
setControlFocus(Date.now())
|
||||
}} className='mr-1 flex items-center space-x-1'>
|
||||
<TypeSelector
|
||||
noLeft
|
||||
triggerClassName='!text-xs'
|
||||
readonly={readonly}
|
||||
DropDownIcon={ChevronDown}
|
||||
value={varKindType}
|
||||
options={varKindTypes}
|
||||
onChange={handleVarKindTypeChange}
|
||||
/>
|
||||
<div className='h-4 w-px bg-black/5'></div>
|
||||
{isAddBtnTrigger
|
||||
? (
|
||||
<div>
|
||||
<AddButton onClick={() => { }}></AddButton>
|
||||
</div>
|
||||
: (!hasValue && <div className='ml-1.5 mr-1'>
|
||||
<Variable02 className='w-3.5 h-3.5 text-gray-400' />
|
||||
</div>)}
|
||||
{isConstant
|
||||
? (
|
||||
<input
|
||||
type='text'
|
||||
className='w-full h-8 leading-8 pl-0.5 bg-transparent text-[13px] font-normal text-gray-900 placeholder:text-gray-400 focus:outline-none overflow-hidden'
|
||||
value={isConstant ? value : ''}
|
||||
onChange={handleStaticChange}
|
||||
onFocus={() => setIsFocus(true)}
|
||||
onBlur={() => setIsFocus(false)}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className={cn('inline-flex h-full items-center px-1.5 rounded-[5px]', hasValue && 'bg-white')}>
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={outputVarNode?.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
||||
maxWidth: maxNodeNameWidth,
|
||||
}}>{outputVarNode?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||
<div className='ml-0.5 text-xs font-medium truncate' title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
||||
maxWidth: maxTypeWidth,
|
||||
}}>{type}</div>
|
||||
</>
|
||||
)
|
||||
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
||||
)
|
||||
: (<div ref={triggerRef} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8 p-1 rounded-lg bg-gray-100 border')}>
|
||||
{isSupportConstantValue
|
||||
? <div onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOpen(false)
|
||||
setControlFocus(Date.now())
|
||||
}} className='mr-1 flex items-center space-x-1'>
|
||||
<TypeSelector
|
||||
noLeft
|
||||
triggerClassName='!text-xs'
|
||||
readonly={readonly}
|
||||
DropDownIcon={ChevronDown}
|
||||
value={varKindType}
|
||||
options={varKindTypes}
|
||||
onChange={handleVarKindTypeChange}
|
||||
/>
|
||||
<div className='h-4 w-px bg-black/5'></div>
|
||||
</div>
|
||||
)}
|
||||
{(hasValue && !readonly) && (<div
|
||||
className='invisible group-hover/wrap:visible absolute h-5 right-1 top-[50%] translate-y-[-50%] group p-1 rounded-md hover:bg-black/5 cursor-pointer'
|
||||
onClick={handleClearVar}
|
||||
>
|
||||
<XClose className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
|
||||
: (!hasValue && <div className='ml-1.5 mr-1'>
|
||||
<Variable02 className='w-3.5 h-3.5 text-gray-400' />
|
||||
</div>)}
|
||||
{isConstant
|
||||
? (
|
||||
<input
|
||||
type='text'
|
||||
className='w-full h-8 leading-8 pl-0.5 bg-transparent text-[13px] font-normal text-gray-900 placeholder:text-gray-400 focus:outline-none overflow-hidden'
|
||||
value={isConstant ? value : ''}
|
||||
onChange={handleStaticChange}
|
||||
onFocus={() => setIsFocus(true)}
|
||||
onBlur={() => setIsFocus(false)}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className={cn('inline-flex h-full items-center px-1.5 rounded-[5px]', hasValue && 'bg-white')}>
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={outputVarNode?.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
||||
maxWidth: maxNodeNameWidth,
|
||||
}}>{outputVarNode?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||
<div className='ml-0.5 text-xs font-medium truncate' title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
||||
maxWidth: maxTypeWidth,
|
||||
}}>{type}</div>
|
||||
</>
|
||||
)
|
||||
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
||||
</div>
|
||||
)}
|
||||
{(hasValue && !readonly) && (<div
|
||||
className='invisible group-hover/wrap:visible absolute h-5 right-1 top-[50%] translate-y-[-50%] group p-1 rounded-md hover:bg-black/5 cursor-pointer'
|
||||
onClick={handleClearVar}
|
||||
>
|
||||
<XClose className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
|
||||
</div>)}
|
||||
</div>)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
|
|
@ -281,7 +314,7 @@ const VarReferencePicker: FC<Props> = ({
|
|||
<VarReferencePopup
|
||||
vars={outputVars}
|
||||
onChange={handleVarReferenceChange}
|
||||
itemWidth={triggerWidth}
|
||||
itemWidth={isAddBtnTrigger ? 260 : triggerWidth}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElemContent>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import VarReferenceVars from './var-reference-vars'
|
||||
import { type NodeOutPutVar, type ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
vars: NodeOutPutVar[]
|
||||
onChange: (value: ValueSelector) => void
|
||||
onChange: (value: ValueSelector, varDetail: Var) => void
|
||||
itemWidth?: number
|
||||
}
|
||||
const VarReferencePopup: FC<Props> = ({
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import useNodeInfo from './use-node-info'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
|
||||
type Params = {
|
||||
onlyLeafNodeVar?: boolean
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
}
|
||||
|
||||
const useAvailableVarList = (nodeId: string, {
|
||||
onlyLeafNodeVar,
|
||||
filterVar,
|
||||
|
|
@ -16,14 +18,29 @@ const useAvailableVarList = (nodeId: string, {
|
|||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)
|
||||
const availableVars = toNodeOutputVars(availableNodes, isChatMode, filterVar)
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
} = useNodeInfo(nodeId)
|
||||
|
||||
const availableVars = toNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
t,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
})
|
||||
|
||||
return {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent: iterationNode ? [...availableNodes, iterationNode] : availableNodes,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { useStoreApi } from 'reactflow'
|
||||
|
||||
const useNodeInfo = (nodeId: string) => {
|
||||
const store = useStoreApi()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const node = allNodes.find(n => n.id === nodeId)
|
||||
const isInIteration = !!node?.data.isInIteration
|
||||
const parentNodeId = node?.parentId
|
||||
const parentNode = allNodes.find(n => n.id === parentNodeId)
|
||||
return {
|
||||
node,
|
||||
isInIteration,
|
||||
parentNode,
|
||||
}
|
||||
}
|
||||
|
||||
export default useNodeInfo
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { unionBy } from 'lodash-es'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodeDataUpdate,
|
||||
|
|
@ -11,7 +12,7 @@ import { getNodeInfoById, isSystemVar, toNodeOutputVars } from '@/app/components
|
|||
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { singleNodeRun } from '@/service/workflow'
|
||||
import { getIterationSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import LLMDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import KnowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
|
||||
|
|
@ -22,7 +23,12 @@ import QuestionClassifyDefault from '@/app/components/workflow/nodes/question-cl
|
|||
import HTTPDefault from '@/app/components/workflow/nodes/http/default'
|
||||
import ToolDefault from '@/app/components/workflow/nodes/tool/default'
|
||||
import VariableAssigner from '@/app/components/workflow/nodes/variable-assigner/default'
|
||||
import ParameterExtractorDefault from '@/app/components/workflow/nodes/parameter-extractor/default'
|
||||
import IterationDefault from '@/app/components/workflow/nodes/iteration/default'
|
||||
import { ssePost } from '@/service/base'
|
||||
|
||||
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
const { checkValid: checkLLMValid } = LLMDefault
|
||||
const { checkValid: checkKnowledgeRetrievalValid } = KnowledgeRetrievalDefault
|
||||
const { checkValid: checkIfElseValid } = IfElseDefault
|
||||
|
|
@ -32,6 +38,8 @@ const { checkValid: checkQuestionClassifyValid } = QuestionClassifyDefault
|
|||
const { checkValid: checkHttpValid } = HTTPDefault
|
||||
const { checkValid: checkToolValid } = ToolDefault
|
||||
const { checkValid: checkVariableAssignerValid } = VariableAssigner
|
||||
const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault
|
||||
const { checkValid: checkIterationValid } = IterationDefault
|
||||
|
||||
const checkValidFns: Record<BlockEnum, Function> = {
|
||||
[BlockEnum.LLM]: checkLLMValid,
|
||||
|
|
@ -43,6 +51,9 @@ const checkValidFns: Record<BlockEnum, Function> = {
|
|||
[BlockEnum.HttpRequest]: checkHttpValid,
|
||||
[BlockEnum.Tool]: checkToolValid,
|
||||
[BlockEnum.VariableAssigner]: checkVariableAssignerValid,
|
||||
[BlockEnum.VariableAggregator]: checkVariableAssignerValid,
|
||||
[BlockEnum.ParameterExtractor]: checkParameterExtractorValid,
|
||||
[BlockEnum.Iteration]: checkIterationValid,
|
||||
} as any
|
||||
|
||||
type Params<T> = {
|
||||
|
|
@ -50,6 +61,7 @@ type Params<T> = {
|
|||
data: CommonNodeType<T>
|
||||
defaultRunInputData: Record<string, any>
|
||||
moreDataForCheckValid?: any
|
||||
iteratorInputKey?: string
|
||||
}
|
||||
|
||||
const varTypeToInputVarType = (type: VarType, {
|
||||
|
|
@ -78,13 +90,16 @@ const useOneStepRun = <T>({
|
|||
data,
|
||||
defaultRunInputData,
|
||||
moreDataForCheckValid,
|
||||
iteratorInputKey,
|
||||
}: Params<T>) => {
|
||||
const { t } = useTranslation()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow() as any
|
||||
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
|
||||
const isChatMode = useIsChatMode()
|
||||
const isIteration = data.type === BlockEnum.Iteration
|
||||
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
const allOutputVars = toNodeOutputVars(getBeforeNodesInSameBranch(id), isChatMode)
|
||||
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
|
||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
const getVar = (valueSelector: ValueSelector): Var | undefined => {
|
||||
let res: Var | undefined
|
||||
const isSystem = valueSelector[0] === 'sys'
|
||||
|
|
@ -95,14 +110,17 @@ const useOneStepRun = <T>({
|
|||
return targetVar.vars.find(item => item.variable.split('.')[1] === valueSelector[1])
|
||||
|
||||
let curr: any = targetVar.vars
|
||||
if (!curr)
|
||||
return
|
||||
|
||||
valueSelector.slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
curr = curr.find((v: any) => v.variable === key)
|
||||
curr = curr?.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
res = curr
|
||||
}
|
||||
else {
|
||||
if (curr.type === VarType.object)
|
||||
if (curr?.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
|
|
@ -113,11 +131,14 @@ const useOneStepRun = <T>({
|
|||
const checkValid = checkValidFns[data.type]
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {})
|
||||
const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey].length : 0
|
||||
const [runResult, setRunResult] = useState<any>(null)
|
||||
|
||||
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
|
||||
const [canShowSingleRun, setCanShowSingleRun] = useState(false)
|
||||
const isShowSingleRun = data._isSingleRun && canShowSingleRun
|
||||
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkValid) {
|
||||
setCanShowSingleRun(true)
|
||||
|
|
@ -152,6 +173,15 @@ const useOneStepRun = <T>({
|
|||
},
|
||||
})
|
||||
}
|
||||
const showSingleRun = () => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
|
||||
const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
|
||||
|
||||
|
|
@ -165,34 +195,117 @@ const useOneStepRun = <T>({
|
|||
})
|
||||
let res: any
|
||||
try {
|
||||
res = await singleNodeRun(appId!, id, { inputs: submitData }) as any
|
||||
if (!isIteration) {
|
||||
res = await singleNodeRun(appId!, id, { inputs: submitData }) as any
|
||||
}
|
||||
else {
|
||||
setIterationRunResult([])
|
||||
let _iterationResult: NodeTracing[][] = []
|
||||
let _runResult: any = null
|
||||
ssePost(
|
||||
getIterationSingleNodeRunUrl(isChatMode, appId!, id),
|
||||
{ body: { inputs: submitData } },
|
||||
{
|
||||
onWorkflowStarted: () => {
|
||||
},
|
||||
onWorkflowFinished: (params) => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Succeeded,
|
||||
},
|
||||
})
|
||||
const { data: iterationData } = params
|
||||
_runResult.created_by = iterationData.created_by.name
|
||||
setRunResult(_runResult)
|
||||
},
|
||||
onIterationNext: () => {
|
||||
// iteration next trigger time is triggered one more time than iterationTimes
|
||||
if (_iterationResult.length >= iterationTimes!)
|
||||
return
|
||||
|
||||
const newIterationRunResult = produce(_iterationResult, (draft) => {
|
||||
draft.push([])
|
||||
})
|
||||
_iterationResult = newIterationRunResult
|
||||
setIterationRunResult(newIterationRunResult)
|
||||
},
|
||||
onIterationFinish: (params) => {
|
||||
_runResult = params.data
|
||||
setRunResult(_runResult)
|
||||
},
|
||||
onNodeStarted: (params) => {
|
||||
const newIterationRunResult = produce(_iterationResult, (draft) => {
|
||||
draft[draft.length - 1].push({
|
||||
...params.data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
})
|
||||
_iterationResult = newIterationRunResult
|
||||
setIterationRunResult(newIterationRunResult)
|
||||
},
|
||||
onNodeFinished: (params) => {
|
||||
const iterationRunResult = _iterationResult
|
||||
|
||||
const { data } = params
|
||||
const currentIndex = iterationRunResult[iterationRunResult.length - 1].findIndex(trace => trace.node_id === data.node_id)
|
||||
const newIterationRunResult = produce(iterationRunResult, (draft) => {
|
||||
if (currentIndex > -1) {
|
||||
draft[draft.length - 1][currentIndex] = {
|
||||
...data,
|
||||
status: NodeRunningStatus.Succeeded,
|
||||
} as NodeTracing
|
||||
}
|
||||
})
|
||||
_iterationResult = newIterationRunResult
|
||||
setIterationRunResult(newIterationRunResult)
|
||||
},
|
||||
onError: () => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Failed,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
if (res.error)
|
||||
throw new Error(res.error)
|
||||
}
|
||||
catch (e: any) {
|
||||
if (!isIteration) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Failed,
|
||||
},
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (!isIteration) {
|
||||
setRunResult({
|
||||
...res,
|
||||
total_tokens: res.execution_metadata?.total_tokens || 0,
|
||||
created_by: res.created_by_account?.name || '',
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!isIteration) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Failed,
|
||||
_singleRunningStatus: NodeRunningStatus.Succeeded,
|
||||
},
|
||||
})
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
setRunResult({
|
||||
...res,
|
||||
total_tokens: res.execution_metadata?.total_tokens || 0,
|
||||
created_by: res.created_by_account?.name || '',
|
||||
})
|
||||
}
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Succeeded,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleStop = () => {
|
||||
|
|
@ -241,12 +354,12 @@ const useOneStepRun = <T>({
|
|||
})
|
||||
|
||||
const variables = unionBy(valueSelectors, item => item.join('.')).map((item) => {
|
||||
const varInfo = getNodeInfoById(availableNodes, item[0])?.data
|
||||
const varInfo = getNodeInfoById(availableNodesIncludeParent, item[0])?.data
|
||||
|
||||
return {
|
||||
label: {
|
||||
nodeType: varInfo?.type,
|
||||
nodeName: varInfo?.title || availableNodes[0]?.data.title, // default start node title
|
||||
nodeName: varInfo?.title || availableNodesIncludeParent[0]?.data.title, // default start node title
|
||||
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
|
||||
},
|
||||
variable: `#${item.join('.')}#`,
|
||||
|
|
@ -261,6 +374,7 @@ const useOneStepRun = <T>({
|
|||
return {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
showSingleRun,
|
||||
toVarInputs,
|
||||
getInputVars,
|
||||
runningStatus,
|
||||
|
|
@ -270,6 +384,7 @@ const useOneStepRun = <T>({
|
|||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
iterationRunResult,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import type {
|
|||
import {
|
||||
cloneElement,
|
||||
memo,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import type { NodeProps } from '../../types'
|
||||
import {
|
||||
BlockEnum,
|
||||
|
|
@ -17,11 +19,14 @@ import {
|
|||
useNodesReadOnly,
|
||||
useToolIcon,
|
||||
} from '../../hooks'
|
||||
import { useNodeIterationInteractions } from '../iteration/use-interactions'
|
||||
import {
|
||||
NodeSourceHandle,
|
||||
NodeTargetHandle,
|
||||
} from './components/node-handle'
|
||||
import NodeResizer from './components/node-resizer'
|
||||
import NodeControl from './components/node-control'
|
||||
import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import {
|
||||
CheckCircle,
|
||||
|
|
@ -40,9 +45,24 @@ const BaseNode: FC<BaseNodeProps> = ({
|
|||
}) => {
|
||||
const nodeRef = useRef<HTMLDivElement>(null)
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeIterationChildSizeChange } = useNodeIterationInteractions()
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const showSelectedBorder = data.selected || data._isBundled
|
||||
useEffect(() => {
|
||||
if (nodeRef.current && data.selected && data.isInIteration) {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleNodeIterationChildSizeChange(id)
|
||||
})
|
||||
|
||||
resizeObserver.observe(nodeRef.current)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
}, [data.isInIteration, data.selected, id, handleNodeIterationChildSizeChange])
|
||||
|
||||
const showSelectedBorder = data.selected || data._isBundled || data._isEntering
|
||||
const {
|
||||
showRunningBorder,
|
||||
showSuccessBorder,
|
||||
|
|
@ -57,26 +77,47 @@ const BaseNode: FC<BaseNodeProps> = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex border-[2px] rounded-2xl
|
||||
${(showSelectedBorder && !data._isInvalidConnection) ? 'border-primary-600' : 'border-transparent'}
|
||||
`}
|
||||
className={cn(
|
||||
'flex border-[2px] rounded-2xl',
|
||||
showSelectedBorder ? 'border-primary-600' : 'border-transparent',
|
||||
)}
|
||||
ref={nodeRef}
|
||||
style={{
|
||||
width: data.type === BlockEnum.Iteration ? data.width : 'auto',
|
||||
height: data.type === BlockEnum.Iteration ? data.height : 'auto',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
group relative pb-1 w-[240px] bg-[#fcfdff] shadow-xs
|
||||
border border-transparent rounded-[15px]
|
||||
${!data._runningStatus && 'hover:shadow-lg'}
|
||||
${showRunningBorder && '!border-primary-500'}
|
||||
${showSuccessBorder && '!border-[#12B76A]'}
|
||||
${showFailedBorder && '!border-[#F04438]'}
|
||||
${data._isInvalidConnection && '!border-[#F04438]'}
|
||||
${data._isBundled && '!shadow-lg'}
|
||||
`}
|
||||
className={cn(
|
||||
'group relative pb-1 shadow-xs',
|
||||
'border border-transparent rounded-[15px]',
|
||||
data.type !== BlockEnum.Iteration && 'w-[240px] bg-[#fcfdff]',
|
||||
data.type === BlockEnum.Iteration && 'flex flex-col w-full h-full bg-[#fcfdff]/80',
|
||||
!data._runningStatus && 'hover:shadow-lg',
|
||||
showRunningBorder && '!border-primary-500',
|
||||
showSuccessBorder && '!border-[#12B76A]',
|
||||
showFailedBorder && '!border-[#F04438]',
|
||||
data._isBundled && '!shadow-lg',
|
||||
)}
|
||||
>
|
||||
{
|
||||
data.type !== BlockEnum.VariableAssigner && !data._isCandidate && (
|
||||
data._showAddVariablePopup && (
|
||||
<AddVariablePopupWithPosition
|
||||
nodeId={id}
|
||||
nodeData={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type === BlockEnum.Iteration && (
|
||||
<NodeResizer
|
||||
nodeId={id}
|
||||
nodeData={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type !== BlockEnum.VariableAssigner && data.type !== BlockEnum.VariableAggregator && !data._isCandidate && (
|
||||
<NodeTargetHandle
|
||||
id={id}
|
||||
data={data}
|
||||
|
|
@ -103,7 +144,10 @@ const BaseNode: FC<BaseNodeProps> = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
<div className='flex items-center px-3 pt-3 pb-2'>
|
||||
<div className={cn(
|
||||
'flex items-center px-3 pt-3 pb-2 rounded-t-2xl',
|
||||
data.type === BlockEnum.Iteration && 'bg-[rgba(250,252,255,0.9)]',
|
||||
)}>
|
||||
<BlockIcon
|
||||
className='shrink-0 mr-2'
|
||||
type={data.type}
|
||||
|
|
@ -116,6 +160,13 @@ const BaseNode: FC<BaseNodeProps> = ({
|
|||
>
|
||||
{data.title}
|
||||
</div>
|
||||
{
|
||||
data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && (
|
||||
<div className='mr-1.5 text-xs font-medium text-primary-600'>
|
||||
{data._iterationIndex}/{data._iterationLength}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
|
||||
<Loading02 className='w-3.5 h-3.5 text-primary-600 animate-spin' />
|
||||
|
|
@ -132,9 +183,20 @@ const BaseNode: FC<BaseNodeProps> = ({
|
|||
)
|
||||
}
|
||||
</div>
|
||||
{cloneElement(children, { id, data })}
|
||||
{
|
||||
data.desc && (
|
||||
data.type !== BlockEnum.Iteration && (
|
||||
cloneElement(children, { id, data })
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type === BlockEnum.Iteration && (
|
||||
<div className='grow pl-1 pr-1 pb-1'>
|
||||
{cloneElement(children, { id, data })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
data.desc && data.type !== BlockEnum.Iteration && (
|
||||
<div className='px-3 pt-1 pb-2 text-xs leading-[18px] text-gray-500 whitespace-pre-line break-words'>
|
||||
{data.desc}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const Panel: FC<NodePanelProps<AnswerNodeType>> = ({
|
|||
filterVar,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const { availableVars, availableNodes } = useAvailableVarList(id, {
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar,
|
||||
})
|
||||
|
|
@ -35,7 +35,7 @@ const Panel: FC<NodePanelProps<AnswerNodeType>> = ({
|
|||
value={inputs.answer}
|
||||
onChange={handleAnswerChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
availableNodes={availableNodesWithParent}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ import ToolNode from './tool/node'
|
|||
import ToolPanel from './tool/panel'
|
||||
import VariableAssignerNode from './variable-assigner/node'
|
||||
import VariableAssignerPanel from './variable-assigner/panel'
|
||||
import ParameterExtractorNode from './parameter-extractor/node'
|
||||
import ParameterExtractorPanel from './parameter-extractor/panel'
|
||||
import IterationNode from './iteration/node'
|
||||
import IterationPanel from './iteration/panel'
|
||||
|
||||
export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartNode,
|
||||
|
|
@ -38,6 +42,9 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
|||
[BlockEnum.HttpRequest]: HttpNode,
|
||||
[BlockEnum.Tool]: ToolNode,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerNode,
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerNode,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
|
||||
[BlockEnum.Iteration]: IterationNode,
|
||||
}
|
||||
|
||||
export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
|
|
@ -53,4 +60,7 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
|||
[BlockEnum.HttpRequest]: HttpPanel,
|
||||
[BlockEnum.Tool]: ToolPanel,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
|
||||
[BlockEnum.Iteration]: IterationPanel,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { EndNodeType } from './types'
|
||||
import type { NodeProps, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
import { isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import type { NodeProps, Variable } from '@/app/components/workflow/types'
|
||||
import { getVarType, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<EndNodeType>> = ({
|
||||
id,
|
||||
|
|
@ -19,7 +19,6 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
|||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
const isChatMode = useIsChatMode()
|
||||
const outputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
|
|
@ -29,27 +28,6 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
|||
return availableNodes.find(node => node.id === id) || startNode
|
||||
}
|
||||
|
||||
const getVarType = (nodeId: string, value: ValueSelector) => {
|
||||
const targetVar = outputVars.find(v => v.nodeId === nodeId)
|
||||
if (!targetVar)
|
||||
return 'undefined'
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
const isSystem = isSystemVar(value);
|
||||
(value).slice(1).forEach((key, i) => {
|
||||
const isLast = i === value.length - 2
|
||||
curr = curr.find((v: any) => v.variable === isSystem ? `sys.${key}` : key)
|
||||
if (isLast) {
|
||||
type = curr.type
|
||||
}
|
||||
else {
|
||||
if (curr.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
return type
|
||||
}
|
||||
const { outputs } = data
|
||||
const filteredOutputs = (outputs as Variable[]).filter(({ value_selector }) => value_selector.length > 0)
|
||||
|
||||
|
|
@ -62,6 +40,11 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
|||
const node = getNode(value_selector[0])
|
||||
const isSystem = isSystemVar(value_selector)
|
||||
const varName = isSystem ? `sys.${value_selector[value_selector.length - 1]}` : value_selector[value_selector.length - 1]
|
||||
const varType = getVarType({
|
||||
valueSelector: value_selector,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
})
|
||||
return (
|
||||
<div key={index} className='flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'>
|
||||
<div className='flex items-center text-xs font-medium text-gray-500'>
|
||||
|
|
@ -79,7 +62,7 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className='text-xs font-normal text-gray-700'>
|
||||
<div className='max-w-[42px] ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={getVarType(node?.id || '', value_selector)}>{getVarType(node?.id || '', value_selector)}</div>
|
||||
<div className='max-w-[42px] ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={varType}>{varType}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ const Item: FC<ItemProps> = ({
|
|||
readonly,
|
||||
nodeId,
|
||||
payload,
|
||||
varType,
|
||||
varType = VarType.string,
|
||||
onChange,
|
||||
canRemove,
|
||||
onRemove = () => { },
|
||||
|
|
@ -184,7 +184,7 @@ const Item: FC<ItemProps> = ({
|
|||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
if (!varType) {
|
||||
if (!payload.variable_selector || payload.variable_selector.length === 0) {
|
||||
e.stopPropagation()
|
||||
Toast.notify({
|
||||
message: t(`${i18nPrefix}.notSetVariable`),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
|
|||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes.filter(type => type !== BlockEnum.VariableAssigner)
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: IfElseNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import produce from 'immer'
|
|||
import type { Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import { getVarType } from '../_base/components/variable/utils'
|
||||
import useNodeInfo from '../_base/hooks/use-node-info'
|
||||
import { LogicalOperator } from './types'
|
||||
import type { Condition, IfElseNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
|
|
@ -15,8 +16,11 @@ import {
|
|||
const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const {
|
||||
parentNode,
|
||||
} = useNodeInfo(id)
|
||||
const isChatMode = useIsChatMode()
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
const beforeNodes = getBeforeNodesInSameBranch(id)
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<IfElseNodeType>(id, payload)
|
||||
|
||||
|
|
@ -51,7 +55,12 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
|||
}, [])
|
||||
|
||||
const varTypesList = (inputs.conditions || []).map((condition) => {
|
||||
return getVarType(condition.variable_selector, availableNodes, isChatMode)
|
||||
return getVarType({
|
||||
parentNode,
|
||||
valueSelector: condition.variable_selector,
|
||||
availableNodes: beforeNodes,
|
||||
isChatMode,
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -8,16 +8,18 @@ import {
|
|||
import BaseNode from './_base/node'
|
||||
import BasePanel from './_base/panel'
|
||||
|
||||
const CustomNode = memo((props: NodeProps) => {
|
||||
const CustomNode = (props: NodeProps) => {
|
||||
const nodeData = props.data
|
||||
const NodeComponent = NodeComponentMap[nodeData.type]
|
||||
|
||||
return (
|
||||
<BaseNode { ...props }>
|
||||
<NodeComponent />
|
||||
</BaseNode>
|
||||
<>
|
||||
<BaseNode { ...props }>
|
||||
<NodeComponent />
|
||||
</BaseNode>
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
CustomNode.displayName = 'CustomNode'
|
||||
|
||||
export const Panel = memo((props: Node) => {
|
||||
|
|
|
|||
128
web/app/components/workflow/nodes/iteration/add-block.tsx
Normal file
128
web/app/components/workflow/nodes/iteration/add-block.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
generateNewNode,
|
||||
} from '../../utils'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesReadOnly,
|
||||
} from '../../hooks'
|
||||
import { NODES_INITIAL_DATA } from '../../constants'
|
||||
import InsertBlock from './insert-block'
|
||||
import type { IterationNodeType } from './types'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { IterationStart } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
type AddBlockProps = {
|
||||
iterationNodeId: string
|
||||
iterationNodeData: IterationNodeType
|
||||
}
|
||||
const AddBlock = ({
|
||||
iterationNodeId,
|
||||
iterationNodeData,
|
||||
}: AddBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, true)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(iterationNodeData.startNodeType, true)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[type],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||
...(toolDefaultValue || {}),
|
||||
isIterationStart: true,
|
||||
isInIteration: true,
|
||||
iteration_id: iterationNodeId,
|
||||
},
|
||||
position: {
|
||||
x: 117,
|
||||
y: 85,
|
||||
},
|
||||
zIndex: 1001,
|
||||
parentId: iterationNodeId,
|
||||
extent: 'parent',
|
||||
})
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.id === iterationNodeId) {
|
||||
node.data._children = [newNode.id]
|
||||
node.data.start_node_id = newNode.id
|
||||
node.data.startNodeType = newNode.data.type
|
||||
}
|
||||
})
|
||||
draft.push(newNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store, t, iterationNodeId])
|
||||
|
||||
const renderTriggerElement = useCallback((open: boolean) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative inline-flex items-center px-3 h-8 rounded-lg border-[0.5px] border-gray-50 bg-white shadow-xs cursor-pointer hover:bg-gray-200 text-[13px] font-medium text-gray-700',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
open && '!bg-gray-50',
|
||||
)}>
|
||||
<Plus className='mr-1 w-4 h-4' />
|
||||
{t('workflow.common.addBlock')}
|
||||
</div>
|
||||
)
|
||||
}, [nodesReadOnly, t])
|
||||
|
||||
return (
|
||||
<div className='absolute top-12 left-6 flex items-center h-8 z-10'>
|
||||
<TooltipPlus popupContent={t('workflow.blocks.iteration-start')}>
|
||||
<div className='flex items-center justify-center w-6 h-6 rounded-full border-[0.5px] border-black/[0.02] shadow-md bg-primary-500'>
|
||||
<IterationStart className='w-4 h-4 text-white' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
<div className='group/insert relative w-16 h-0.5 bg-gray-300'>
|
||||
{
|
||||
iterationNodeData.startNodeType && (
|
||||
<InsertBlock
|
||||
startNodeId={iterationNodeData.start_node_id}
|
||||
availableBlocksTypes={availablePrevBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className='absolute right-0 top-1/2 -translate-y-1/2 w-0.5 h-2 bg-primary-500'></div>
|
||||
</div>
|
||||
{
|
||||
!iterationNodeData.startNodeType && (
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTriggerElement}
|
||||
triggerInnerClassName='inline-flex'
|
||||
popupClassName='!min-w-[256px]'
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddBlock)
|
||||
39
web/app/components/workflow/nodes/iteration/default.ts
Normal file
39
web/app/components/workflow/nodes/iteration/default.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { IterationNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<IterationNodeType> = {
|
||||
defaultValue: {
|
||||
start_node_id: '',
|
||||
iterator_selector: [],
|
||||
output_selector: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: IterationNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
if (!errorMessages && (!payload.iterator_selector || payload.iterator_selector.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.iteration.input`) })
|
||||
|
||||
if (!errorMessages && (!payload.output_selector || payload.output_selector.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.iteration.output`) })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
61
web/app/components/workflow/nodes/iteration/insert-block.tsx
Normal file
61
web/app/components/workflow/nodes/iteration/insert-block.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useNodesInteractions } from '../../hooks'
|
||||
import type {
|
||||
BlockEnum,
|
||||
OnSelectBlock,
|
||||
} from '../../types'
|
||||
import BlockSelector from '../../block-selector'
|
||||
|
||||
type InsertBlockProps = {
|
||||
startNodeId: string
|
||||
availableBlocksTypes: BlockEnum[]
|
||||
}
|
||||
const InsertBlock = ({
|
||||
startNodeId,
|
||||
availableBlocksTypes,
|
||||
}: InsertBlockProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleInsert = useCallback<OnSelectBlock>((nodeType, toolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType,
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
nextNodeId: startNodeId,
|
||||
nextNodeTargetHandle: 'target',
|
||||
},
|
||||
)
|
||||
}, [startNodeId, handleNodeAdd])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'nopan nodrag',
|
||||
'hidden group-hover/insert:block absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2',
|
||||
open && '!block',
|
||||
)}
|
||||
>
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
triggerClassName={() => 'hover:scale-125 transition-all'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(InsertBlock)
|
||||
49
web/app/components/workflow/nodes/iteration/node.tsx
Normal file
49
web/app/components/workflow/nodes/iteration/node.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
Background,
|
||||
useNodesInitialized,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import cn from 'classnames'
|
||||
import { useNodeIterationInteractions } from './use-interactions'
|
||||
import type { IterationNodeType } from './types'
|
||||
import AddBlock from './add-block'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<IterationNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { zoom } = useViewport()
|
||||
const nodesInitialized = useNodesInitialized()
|
||||
const { handleNodeIterationRerender } = useNodeIterationInteractions()
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesInitialized)
|
||||
handleNodeIterationRerender(id)
|
||||
}, [nodesInitialized, id, handleNodeIterationRerender])
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative min-w-[258px] min-h-[118px] w-full h-full rounded-2xl bg-[#F0F2F7]/90',
|
||||
)}>
|
||||
<Background
|
||||
id={`iteration-background-${id}`}
|
||||
className='rounded-2xl !z-0'
|
||||
gap={[14 / zoom, 14 / zoom]}
|
||||
size={2 / zoom}
|
||||
color='#E4E5E7'
|
||||
/>
|
||||
<AddBlock
|
||||
iterationNodeId={id}
|
||||
iterationNodeData={data}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Node)
|
||||
136
web/app/components/workflow/nodes/iteration/panel.tsx
Normal file
136
web/app/components/workflow/nodes/iteration/panel.tsx
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import Split from '../_base/components/split'
|
||||
import ResultPanel from '../../run/result-panel'
|
||||
import IterationResultPanel from '../../run/iteration-result-panel'
|
||||
import type { IterationNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import { ArrowNarrowRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.iteration'
|
||||
|
||||
const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterInputVar,
|
||||
handleInputChange,
|
||||
childrenNodeVars,
|
||||
iterationChildrenNodes,
|
||||
handleOutputVarChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
isShowIterationDetail,
|
||||
backToSingleRun,
|
||||
showIterationDetail,
|
||||
hideIterationDetail,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
usedOutVars,
|
||||
iterator,
|
||||
setIterator,
|
||||
iteratorInputKey,
|
||||
iterationRunResult,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.input`)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.iterator_selector || []}
|
||||
onChange={handleInputChange}
|
||||
filterVar={filterInputVar}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='mt-2 px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.output`)}
|
||||
operations={(
|
||||
<div className='flex items-center h-[18px] px-1 border border-black/8 rounded-[5px] text-xs font-medium text-gray-500 capitalize'>Array</div>
|
||||
)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.output_selector || []}
|
||||
onChange={handleOutputVarChange}
|
||||
availableNodes={iterationChildrenNodes}
|
||||
availableVars={childrenNodeVars}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{isShowSingleRun && (
|
||||
<BeforeRunForm
|
||||
nodeName={inputs.title}
|
||||
onHide={hideSingleRun}
|
||||
forms={[
|
||||
{
|
||||
inputs: [...usedOutVars],
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
{
|
||||
label: t(`${i18nPrefix}.input`)!,
|
||||
inputs: [{
|
||||
label: '',
|
||||
variable: iteratorInputKey,
|
||||
type: InputVarType.iterator,
|
||||
required: false,
|
||||
}],
|
||||
values: { [iteratorInputKey]: iterator },
|
||||
onChange: keyValue => setIterator((keyValue as any)[iteratorInputKey]),
|
||||
},
|
||||
]}
|
||||
runningStatus={runningStatus}
|
||||
onRun={handleRun}
|
||||
onStop={handleStop}
|
||||
result={
|
||||
<div className='mt-3'>
|
||||
<div className='px-4'>
|
||||
<div className='flex items-center h-[34px] justify-between px-3 bg-gray-100 border-[0.5px] border-gray-200 rounded-lg cursor-pointer' onClick={showIterationDetail}>
|
||||
<div className='leading-[18px] text-[13px] font-medium text-gray-700'>{t(`${i18nPrefix}.iteration`, { count: iterationRunResult.length })}</div>
|
||||
<ArrowNarrowRight className='w-3.5 h-3.5 text-gray-500' />
|
||||
</div>
|
||||
<Split className='mt-3' />
|
||||
</div>
|
||||
<ResultPanel {...runResult} showSteps={false} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isShowIterationDetail && (
|
||||
<IterationResultPanel
|
||||
onBack={backToSingleRun}
|
||||
onHide={hideIterationDetail}
|
||||
list={iterationRunResult}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
15
web/app/components/workflow/nodes/iteration/types.ts
Normal file
15
web/app/components/workflow/nodes/iteration/types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type {
|
||||
BlockEnum,
|
||||
CommonNodeType,
|
||||
ValueSelector,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
export type IterationNodeType = CommonNodeType & {
|
||||
startNodeType?: BlockEnum
|
||||
start_node_id: string // start node id in the iteration
|
||||
iteration_id?: string
|
||||
iterator_selector: ValueSelector
|
||||
output_selector: ValueSelector
|
||||
output_type: VarType // output type.
|
||||
}
|
||||
215
web/app/components/workflow/nodes/iteration/use-config.ts
Normal file
215
web/app/components/workflow/nodes/iteration/use-config.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useIsNodeInIteration,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { VarType } from '../../types'
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils'
|
||||
import useOneStepRun from '../_base/hooks/use-one-step-run'
|
||||
import type { IterationNodeType } from './types'
|
||||
import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
|
||||
const DELIMITER = '@@@@@'
|
||||
const useConfig = (id: string, payload: IterationNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { isNodeInIteration } = useIsNodeInIteration(id)
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const handleInputChange = useCallback((input: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.iterator_selector = input as ValueSelector || []
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// output
|
||||
const { getIterationNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const beforeNodes = getBeforeNodesInSameBranch(id)
|
||||
const iterationChildrenNodes = getIterationNodeChildren(id)
|
||||
const canChooseVarNodes = [...beforeNodes, ...iterationChildrenNodes]
|
||||
const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode)
|
||||
|
||||
const handleOutputVarChange = useCallback((output: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.output_selector = output as ValueSelector || []
|
||||
const outputItemType = varInfo?.type || VarType.string
|
||||
|
||||
draft.output_type = ({
|
||||
[VarType.string]: VarType.arrayString,
|
||||
[VarType.number]: VarType.arrayNumber,
|
||||
[VarType.object]: VarType.arrayObject,
|
||||
} as Record<VarType, VarType>)[outputItemType] || VarType.arrayString
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// single run
|
||||
const iteratorInputKey = `${id}.input_selector`
|
||||
const {
|
||||
isShowSingleRun,
|
||||
showSingleRun,
|
||||
hideSingleRun,
|
||||
toVarInputs,
|
||||
runningStatus,
|
||||
handleRun: doHandleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
iterationRunResult,
|
||||
} = useOneStepRun<IterationNodeType>({
|
||||
id,
|
||||
data: inputs,
|
||||
iteratorInputKey,
|
||||
defaultRunInputData: {
|
||||
[iteratorInputKey]: [''],
|
||||
},
|
||||
})
|
||||
|
||||
const [isShowIterationDetail, {
|
||||
setTrue: doShowIterationDetail,
|
||||
setFalse: doHideIterationDetail,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const hideIterationDetail = useCallback(() => {
|
||||
hideSingleRun()
|
||||
doHideIterationDetail()
|
||||
}, [doHideIterationDetail, hideSingleRun])
|
||||
|
||||
const showIterationDetail = useCallback(() => {
|
||||
doShowIterationDetail()
|
||||
}, [doShowIterationDetail])
|
||||
|
||||
const backToSingleRun = useCallback(() => {
|
||||
hideIterationDetail()
|
||||
showSingleRun()
|
||||
}, [hideIterationDetail, showSingleRun])
|
||||
|
||||
const { usedOutVars, allVarObject } = (() => {
|
||||
const vars: ValueSelector[] = []
|
||||
const varObjs: Record<string, boolean> = {}
|
||||
const allVarObject: Record<string, {
|
||||
inSingleRunPassedKey: string
|
||||
}> = {}
|
||||
iterationChildrenNodes.forEach((node) => {
|
||||
const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0)
|
||||
nodeVars.forEach((varSelector) => {
|
||||
if (varSelector[0] === id) { // skip iteration node itself variable: item, index
|
||||
return
|
||||
}
|
||||
const isInIteration = isNodeInIteration(varSelector[0])
|
||||
if (isInIteration) // not pass iteration inner variable
|
||||
return
|
||||
|
||||
const varSectorStr = varSelector.join('.')
|
||||
if (!varObjs[varSectorStr]) {
|
||||
varObjs[varSectorStr] = true
|
||||
vars.push(varSelector)
|
||||
}
|
||||
let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector)
|
||||
if (typeof passToServerKeys === 'string')
|
||||
passToServerKeys = [passToServerKeys]
|
||||
|
||||
passToServerKeys.forEach((key: string, index: number) => {
|
||||
allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = {
|
||||
inSingleRunPassedKey: key,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
const res = toVarInputs(vars.map((item) => {
|
||||
const varInfo = getNodeInfoById(canChooseVarNodes, item[0])
|
||||
return {
|
||||
label: {
|
||||
nodeType: varInfo?.data.type,
|
||||
nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title
|
||||
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
|
||||
},
|
||||
variable: `${item.join('.')}`,
|
||||
value_selector: item,
|
||||
}
|
||||
}))
|
||||
return {
|
||||
usedOutVars: res,
|
||||
allVarObject,
|
||||
}
|
||||
})()
|
||||
|
||||
const handleRun = useCallback((data: Record<string, any>) => {
|
||||
const formattedData: Record<string, any> = {}
|
||||
Object.keys(allVarObject).forEach((key) => {
|
||||
const [varSectorStr, nodeId] = key.split(DELIMITER)
|
||||
formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr]
|
||||
})
|
||||
formattedData[iteratorInputKey] = data[iteratorInputKey]
|
||||
doHandleRun(formattedData)
|
||||
}, [allVarObject, doHandleRun, iteratorInputKey])
|
||||
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.filter(key => ![iteratorInputKey].includes(key))
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
const newVars = {
|
||||
...newPayload,
|
||||
[iteratorInputKey]: runInputData[iteratorInputKey],
|
||||
}
|
||||
setRunInputData(newVars)
|
||||
}, [iteratorInputKey, runInputData, setRunInputData])
|
||||
|
||||
const iterator = runInputData[iteratorInputKey]
|
||||
const setIterator = useCallback((newIterator: string[]) => {
|
||||
setRunInputData({
|
||||
...runInputData,
|
||||
[iteratorInputKey]: newIterator,
|
||||
})
|
||||
}, [iteratorInputKey, runInputData, setRunInputData])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterInputVar,
|
||||
handleInputChange,
|
||||
childrenNodeVars,
|
||||
iterationChildrenNodes,
|
||||
handleOutputVarChange,
|
||||
isShowSingleRun,
|
||||
showSingleRun,
|
||||
hideSingleRun,
|
||||
isShowIterationDetail,
|
||||
showIterationDetail,
|
||||
hideIterationDetail,
|
||||
backToSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
usedOutVars,
|
||||
iterator,
|
||||
setIterator,
|
||||
iteratorInputKey,
|
||||
iterationRunResult,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
142
web/app/components/workflow/nodes/iteration/use-interactions.ts
Normal file
142
web/app/components/workflow/nodes/iteration/use-interactions.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import { generateNewNode } from '../../utils'
|
||||
import {
|
||||
ITERATION_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../../constants'
|
||||
|
||||
export const useNodeIterationInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
|
||||
const handleNodeIterationRerender = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId)
|
||||
let rightNode: Node
|
||||
let bottomNode: Node
|
||||
|
||||
childrenNodes.forEach((n) => {
|
||||
if (rightNode) {
|
||||
if (n.position.x + n.width! > rightNode.position.x + rightNode.width!)
|
||||
rightNode = n
|
||||
}
|
||||
else {
|
||||
rightNode = n
|
||||
}
|
||||
if (bottomNode) {
|
||||
if (n.position.y + n.height! > bottomNode.position.y + bottomNode.height!)
|
||||
bottomNode = n
|
||||
}
|
||||
else {
|
||||
bottomNode = n
|
||||
}
|
||||
})
|
||||
|
||||
const widthShouldExtend = rightNode! && currentNode.width! < rightNode.position.x + rightNode.width!
|
||||
const heightShouldExtend = bottomNode! && currentNode.height! < bottomNode.position.y + bottomNode.height!
|
||||
|
||||
if (widthShouldExtend || heightShouldExtend) {
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((n) => {
|
||||
if (n.id === nodeId) {
|
||||
if (widthShouldExtend) {
|
||||
n.data.width = rightNode.position.x + rightNode.width! + ITERATION_PADDING.right
|
||||
n.width = rightNode.position.x + rightNode.width! + ITERATION_PADDING.right
|
||||
}
|
||||
if (heightShouldExtend) {
|
||||
n.data.height = bottomNode.position.y + bottomNode.height! + ITERATION_PADDING.bottom
|
||||
n.height = bottomNode.position.y + bottomNode.height! + ITERATION_PADDING.bottom
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleNodeIterationChildDrag = useCallback((node: Node) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
const restrictPosition: { x?: number; y?: number } = { x: undefined, y: undefined }
|
||||
|
||||
if (node.data.isInIteration) {
|
||||
const parentNode = nodes.find(n => n.id === node.parentId)
|
||||
|
||||
if (parentNode) {
|
||||
if (node.position.y < ITERATION_PADDING.top)
|
||||
restrictPosition.y = ITERATION_PADDING.top
|
||||
if (node.position.x < ITERATION_PADDING.left)
|
||||
restrictPosition.x = ITERATION_PADDING.left
|
||||
if (node.position.x + node.width! > parentNode!.width! - ITERATION_PADDING.right)
|
||||
restrictPosition.x = parentNode!.width! - ITERATION_PADDING.right - node.width!
|
||||
if (node.position.y + node.height! > parentNode!.height! - ITERATION_PADDING.bottom)
|
||||
restrictPosition.y = parentNode!.height! - ITERATION_PADDING.bottom - node.height!
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrictPosition,
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleNodeIterationChildSizeChange = useCallback((nodeId: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const parentId = currentNode.parentId
|
||||
|
||||
if (parentId)
|
||||
handleNodeIterationRerender(parentId)
|
||||
}, [store, handleNodeIterationRerender])
|
||||
|
||||
const handleNodeIterationChildrenCopy = useCallback((nodeId: string, newNodeId: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId)
|
||||
|
||||
return childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${childNodeType}`),
|
||||
},
|
||||
position: child.position,
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNodeId,
|
||||
extent: child.extent,
|
||||
zIndex: child.zIndex,
|
||||
})
|
||||
newNode.id = `${newNodeId}${newNode.id + index}`
|
||||
return newNode
|
||||
})
|
||||
}, [store, t])
|
||||
|
||||
return {
|
||||
handleNodeIterationRerender,
|
||||
handleNodeIterationChildDrag,
|
||||
handleNodeIterationChildSizeChange,
|
||||
handleNodeIterationChildrenCopy,
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ const ConfigPrompt: FC<Props> = ({
|
|||
: []
|
||||
const {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent,
|
||||
} = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar,
|
||||
|
|
@ -186,7 +186,7 @@ const ConfigPrompt: FC<Props> = ({
|
|||
isShowContext={isShowContext}
|
||||
hasSetBlockStatus={hasSetBlockStatus}
|
||||
availableVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
availableNodes={availableNodesWithParent}
|
||||
varList={varList}
|
||||
handleAddVariable={handleAddVariable}
|
||||
/>
|
||||
|
|
@ -218,7 +218,7 @@ const ConfigPrompt: FC<Props> = ({
|
|||
isShowContext={isShowContext}
|
||||
hasSetBlockStatus={hasSetBlockStatus}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
availableNodes={availableNodesWithParent}
|
||||
isSupportJinja
|
||||
editionType={(payload as PromptItem).edition_type}
|
||||
varList={varList}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||
filterInputVar,
|
||||
filterVar,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent,
|
||||
isShowVars,
|
||||
handlePromptChange,
|
||||
handleAddEmptyVariable,
|
||||
|
|
@ -234,7 +234,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
|||
isChatModel
|
||||
hasSetBlockStatus={hasSetBlockStatus}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
availableNodes={availableNodesWithParent}
|
||||
/>
|
||||
|
||||
{inputs.memory.query_prompt_template && !inputs.memory.query_prompt_template.includes('{{#sys.query#}}') && (
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
|||
|
||||
const {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent,
|
||||
} = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar,
|
||||
|
|
@ -437,7 +437,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
|||
filterInputVar,
|
||||
filterVar,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent,
|
||||
handlePromptChange,
|
||||
handleMemoryChange,
|
||||
handleSyeQueryChange,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { type ParameterExtractorNodeType, ReasoningModeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<ParameterExtractorNodeType> = {
|
||||
defaultValue: {
|
||||
query: [],
|
||||
model: {
|
||||
provider: '',
|
||||
name: '',
|
||||
mode: 'chat',
|
||||
completion_params: {
|
||||
temperature: 0.7,
|
||||
},
|
||||
},
|
||||
reasoning_mode: ReasoningModeType.prompt,
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: ParameterExtractorNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
if (!errorMessages && (!payload.query || payload.query.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.inputVar`) })
|
||||
|
||||
if (!errorMessages && !payload.model.provider)
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.model`) })
|
||||
|
||||
if (!errorMessages && (!payload.parameters || payload.parameters.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.extractParameters`) })
|
||||
|
||||
if (!errorMessages) {
|
||||
payload.parameters.forEach((param) => {
|
||||
if (errorMessages)
|
||||
return
|
||||
if (!param.name) {
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.addExtractParameterContent.namePlaceholder`) })
|
||||
return
|
||||
}
|
||||
if (!param.type) {
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.addExtractParameterContent.typePlaceholder`) })
|
||||
return
|
||||
}
|
||||
if (!param.description)
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.addExtractParameterContent.descriptionPlaceholder`) })
|
||||
})
|
||||
}
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { ParameterExtractorNodeType } from './types'
|
||||
import {
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<ParameterExtractorNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { provider, name: modelId } = data.model || {}
|
||||
const {
|
||||
textGenerationModelList,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList()
|
||||
const hasSetModel = provider && modelId
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
{hasSetModel && (
|
||||
<ModelSelector
|
||||
defaultModel={{ provider, model: modelId }}
|
||||
modelList={textGenerationModelList}
|
||||
readonly
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
227
web/app/components/workflow/nodes/parameter-extractor/panel.tsx
Normal file
227
web/app/components/workflow/nodes/parameter-extractor/panel.tsx
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MemoryConfig from '../_base/components/memory-config'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import Editor from '../_base/components/prompt/editor'
|
||||
import ResultPanel from '../../run/result-panel'
|
||||
import useConfig from './use-config'
|
||||
import type { ParameterExtractorNodeType } from './types'
|
||||
import ExtractParameter from './components/extract-parameter/list'
|
||||
import ImportFromTool from './components/extract-parameter/import-from-tool'
|
||||
import AddExtractParameter from './components/extract-parameter/update'
|
||||
import ReasoningModePicker from './components/reasoning-mode-picker'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.parameterExtractor'
|
||||
const i18nCommonPrefix = 'workflow.common'
|
||||
|
||||
const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleInputVarChange,
|
||||
filterVar,
|
||||
isChatModel,
|
||||
isChatMode,
|
||||
isCompletionModel,
|
||||
handleModelChanged,
|
||||
handleImportFromTool,
|
||||
handleCompletionParamsChange,
|
||||
addExtractParameter,
|
||||
handleExactParamsChange,
|
||||
handleInstructionChange,
|
||||
hasSetBlockStatus,
|
||||
handleMemoryChange,
|
||||
isSupportFunctionCall,
|
||||
handleReasoningModeChange,
|
||||
availableVars,
|
||||
availableNodesWithParent,
|
||||
inputVarValues,
|
||||
varInputs,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
setInputVarValues,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const model = inputs.model
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVar`)}
|
||||
>
|
||||
<>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.query || []}
|
||||
onChange={handleInputVarChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nCommonPrefix}.model`)}
|
||||
>
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[387px]'
|
||||
isInWorkflow
|
||||
isAdvancedMode={true}
|
||||
mode={model?.mode}
|
||||
provider={model?.provider}
|
||||
completionParams={model?.completion_params}
|
||||
modelId={model?.name}
|
||||
setModel={handleModelChanged}
|
||||
onCompletionParamsChange={handleCompletionParamsChange}
|
||||
hideDebugWithMultipleModel
|
||||
debugWithMultipleModel={false}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.extractParameters`)}
|
||||
operations={
|
||||
!readOnly
|
||||
? (
|
||||
<div className='flex items-center space-x-1'>
|
||||
{!readOnly && (
|
||||
<ImportFromTool onImport={handleImportFromTool} />
|
||||
)}
|
||||
{!readOnly && (<div className='w-px h-3 bg-gray-200'></div>)}
|
||||
<AddExtractParameter type='add' onSave={addExtractParameter} />
|
||||
</div>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ExtractParameter
|
||||
readonly={readOnly}
|
||||
list={inputs.parameters || []}
|
||||
onChange={handleExactParamsChange}
|
||||
/>
|
||||
</Field>
|
||||
<Editor
|
||||
title={
|
||||
<div className='flex items-center space-x-1'>
|
||||
<span className='uppercase'>{t(`${i18nPrefix}.instruction`)}</span>
|
||||
<TooltipPlus popupContent={
|
||||
<div className='w-[120px]'>
|
||||
{t(`${i18nPrefix}.instructionTip`)}
|
||||
</div>}>
|
||||
<HelpCircle className='w-3.5 h-3.5 ml-0.5 text-gray-400' />
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
}
|
||||
value={inputs.instruction}
|
||||
onChange={handleInstructionChange}
|
||||
readOnly={readOnly}
|
||||
isChatModel={isChatModel}
|
||||
isChatApp={isChatMode}
|
||||
isShowContext={false}
|
||||
hasSetBlockStatus={hasSetBlockStatus}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
/>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.advancedSetting`)}
|
||||
supportFold
|
||||
>
|
||||
<>
|
||||
|
||||
{/* Memory */}
|
||||
{isChatMode && (
|
||||
<div className='mt-4'>
|
||||
<MemoryConfig
|
||||
readonly={readOnly}
|
||||
config={{ data: inputs.memory }}
|
||||
onChange={handleMemoryChange}
|
||||
canSetRoleName={isCompletionModel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isSupportFunctionCall && (
|
||||
<div className='mt-2'>
|
||||
<ReasoningModePicker
|
||||
type={inputs.reasoning_mode}
|
||||
onChange={handleReasoningModeChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
|
||||
</div>
|
||||
{inputs.parameters?.length > 0 && (<>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='__is_success'
|
||||
type={VarType.number}
|
||||
description={t(`${i18nPrefix}.isSuccess`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='__reason'
|
||||
type={VarType.string}
|
||||
description={t(`${i18nPrefix}.errorReason`)}
|
||||
/>
|
||||
{inputs.parameters.map((param, index) => (
|
||||
<VarItem
|
||||
key={index}
|
||||
name={param.name}
|
||||
type={param.type}
|
||||
description={param.description}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
</>)}
|
||||
{isShowSingleRun && (
|
||||
<BeforeRunForm
|
||||
nodeName={inputs.title}
|
||||
onHide={hideSingleRun}
|
||||
forms={[
|
||||
{
|
||||
inputs: [{
|
||||
label: t(`${i18nPrefix}.inputVar`)!,
|
||||
variable: 'query',
|
||||
type: InputVarType.paragraph,
|
||||
required: true,
|
||||
}, ...varInputs],
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
]}
|
||||
runningStatus={runningStatus}
|
||||
onRun={handleRun}
|
||||
onStop={handleStop}
|
||||
result={<ResultPanel {...runResult} showSteps={false} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import type { CommonNodeType, Memory, ModelConfig, ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
export enum ParamType {
|
||||
string = 'string',
|
||||
number = 'number',
|
||||
bool = 'bool',
|
||||
select = 'select',
|
||||
arrayString = 'array[string]',
|
||||
arrayNumber = 'array[number]',
|
||||
arrayObject = 'array[object]',
|
||||
}
|
||||
|
||||
export type Param = {
|
||||
name: string
|
||||
type: ParamType
|
||||
options?: string[]
|
||||
description: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export enum ReasoningModeType {
|
||||
prompt = 'prompt',
|
||||
functionCall = 'function_call',
|
||||
}
|
||||
|
||||
export type ParameterExtractorNodeType = CommonNodeType & {
|
||||
model: ModelConfig
|
||||
query: ValueSelector
|
||||
reasoning_mode: ReasoningModeType
|
||||
parameters: Param[]
|
||||
instruction: string
|
||||
memory?: Memory
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { Memory, MoreInfo, ValueSelector, Var } from '../../types'
|
||||
import { ChangeType, VarType } from '../../types'
|
||||
import { useStore } from '../../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import useOneStepRun from '../_base/hooks/use-one-step-run'
|
||||
import type { Param, ParameterExtractorNodeType, ReasoningModeType } from './types'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
|
||||
const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { handleOutVarRenameChange } = useWorkflow()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
|
||||
|
||||
const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
|
||||
const { inputs, setInputs: doSetInputs } = useNodeCrud<ParameterExtractorNodeType>(id, payload)
|
||||
const inputRef = useRef(inputs)
|
||||
|
||||
const setInputs = useCallback((newInputs: ParameterExtractorNodeType) => {
|
||||
if (newInputs.memory && !newInputs.memory.role_prefix) {
|
||||
const newPayload = produce(newInputs, (draft) => {
|
||||
draft.memory!.role_prefix = defaultRolePrefix
|
||||
})
|
||||
doSetInputs(newPayload)
|
||||
inputRef.current = newPayload
|
||||
return
|
||||
}
|
||||
doSetInputs(newInputs)
|
||||
inputRef.current = newInputs
|
||||
}, [doSetInputs, defaultRolePrefix])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.string].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const handleInputVarChange = useCallback((newInputVar: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.query = newInputVar as ValueSelector || []
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleExactParamsChange = useCallback((newParams: Param[], moreInfo?: MoreInfo) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.parameters = newParams
|
||||
})
|
||||
setInputs(newInputs)
|
||||
|
||||
if (moreInfo && moreInfo?.type === ChangeType.changeVarName && moreInfo.payload)
|
||||
handleOutVarRenameChange(id, [id, moreInfo.payload.beforeKey], [id, moreInfo.payload.afterKey!])
|
||||
}, [handleOutVarRenameChange, id, inputs, setInputs])
|
||||
|
||||
const addExtractParameter = useCallback((payload: Param) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
if (!draft.parameters)
|
||||
draft.parameters = []
|
||||
draft.parameters.push(payload)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// model
|
||||
const model = inputs.model || {
|
||||
provider: '',
|
||||
name: '',
|
||||
mode: 'chat',
|
||||
completion_params: {
|
||||
temperature: 0.7,
|
||||
},
|
||||
}
|
||||
const modelMode = inputs.model?.mode
|
||||
const isChatModel = modelMode === 'chat'
|
||||
|
||||
const isCompletionModel = !isChatModel
|
||||
|
||||
const appendDefaultPromptConfig = useCallback((draft: ParameterExtractorNodeType, defaultConfig: any, _passInIsChatMode?: boolean) => {
|
||||
const promptTemplates = defaultConfig.prompt_templates
|
||||
if (!isChatModel) {
|
||||
setDefaultRolePrefix({
|
||||
user: promptTemplates.completion_model.conversation_histories_role.user_prefix,
|
||||
assistant: promptTemplates.completion_model.conversation_histories_role.assistant_prefix,
|
||||
})
|
||||
}
|
||||
}, [isChatModel])
|
||||
|
||||
// const [modelChanged, setModelChanged] = useState(false)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||
|
||||
const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
draft.model.provider = model.provider
|
||||
draft.model.name = model.modelId
|
||||
draft.model.mode = model.mode!
|
||||
const isModeChange = model.mode !== inputRef.current.model?.mode
|
||||
if (isModeChange && defaultConfig && Object.keys(defaultConfig).length > 0)
|
||||
appendDefaultPromptConfig(draft, defaultConfig, model.mode === 'chat')
|
||||
})
|
||||
setInputs(newInputs)
|
||||
// setModelChanged(true)
|
||||
}, [setInputs, defaultConfig, appendDefaultPromptConfig])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProvider?.provider && currentModel?.model && !model.provider) {
|
||||
handleModelChanged({
|
||||
provider: currentProvider?.provider,
|
||||
modelId: currentModel?.model,
|
||||
mode: currentModel?.model_properties?.mode as string,
|
||||
})
|
||||
}
|
||||
}, [model?.provider, currentProvider, currentModel, handleModelChanged])
|
||||
|
||||
const {
|
||||
currentModel: currModel,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{
|
||||
provider: model.provider,
|
||||
model: model.name,
|
||||
},
|
||||
)
|
||||
|
||||
const isSupportFunctionCall = currModel?.features?.includes(ModelFeatureEnum.toolCall) || currModel?.features?.includes(ModelFeatureEnum.multiToolCall)
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.number, VarType.string].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const {
|
||||
availableVars,
|
||||
availableNodesWithParent,
|
||||
} = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: filterInputVar,
|
||||
})
|
||||
|
||||
const handleCompletionParamsChange = useCallback((newParams: Record<string, any>) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.model.completion_params = newParams
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleInstructionChange = useCallback((newInstruction: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.instruction = newInstruction
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const hasSetBlockStatus = {
|
||||
history: false,
|
||||
query: isChatMode ? checkHasQueryBlock(inputs.instruction) : false,
|
||||
context: false,
|
||||
}
|
||||
|
||||
const handleMemoryChange = useCallback((newMemory?: Memory) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.memory = newMemory
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleReasoningModeChange = useCallback((newReasoningMode: ReasoningModeType) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.reasoning_mode = newReasoningMode
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleImportFromTool = useCallback((params: Param[]) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.parameters = params
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// single run
|
||||
const {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
getInputVars,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
} = useOneStepRun<ParameterExtractorNodeType>({
|
||||
id,
|
||||
data: inputs,
|
||||
defaultRunInputData: {
|
||||
query: '',
|
||||
},
|
||||
})
|
||||
|
||||
const varInputs = getInputVars([inputs.instruction])
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
setRunInputData(newPayload)
|
||||
}, [setRunInputData])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
handleInputVarChange,
|
||||
filterVar,
|
||||
isChatMode,
|
||||
inputs,
|
||||
isChatModel,
|
||||
isCompletionModel,
|
||||
handleModelChanged,
|
||||
handleCompletionParamsChange,
|
||||
handleImportFromTool,
|
||||
handleExactParamsChange,
|
||||
addExtractParameter,
|
||||
handleInstructionChange,
|
||||
hasSetBlockStatus,
|
||||
availableVars,
|
||||
availableNodesWithParent,
|
||||
isSupportFunctionCall,
|
||||
handleReasoningModeChange,
|
||||
handleMemoryChange,
|
||||
varInputs,
|
||||
inputVarValues,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
setInputVarValues,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
|
|
@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'
|
|||
import TextEditor from '../../_base/components/editor/text-editor'
|
||||
import MemoryConfig from '../../_base/components/memory-config'
|
||||
import type { Memory } from '@/app/components/workflow/types'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
const i18nPrefix = 'workflow.nodes.questionClassifiers'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -30,7 +32,17 @@ const AdvancedSetting: FC<Props> = ({
|
|||
<>
|
||||
<TextEditor
|
||||
isInNode
|
||||
title={t(`${i18nPrefix}.instruction`)!}
|
||||
title={
|
||||
<div className='flex items-center space-x-1'>
|
||||
<span className='uppercase'>{t(`${i18nPrefix}.instruction`)}</span>
|
||||
<TooltipPlus popupContent={
|
||||
<div className='w-[120px]'>
|
||||
{t(`${i18nPrefix}.instructionTip`)}
|
||||
</div>}>
|
||||
<HelpCircle className='w-3.5 h-3.5 ml-0.5 text-gray-400' />
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
}
|
||||
value={instruction}
|
||||
onChange={onInstructionChange}
|
||||
minHeight={160}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
|
|||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes.filter(type => type !== BlockEnum.VariableAssigner)
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: QuestionClassifierNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
|
|
|||
|
|
@ -36,17 +36,6 @@ const InputVarList: FC<Props> = ({
|
|||
filterVar,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
|
||||
// const valueList = (() => {
|
||||
// const list = []
|
||||
// Object.keys(value).forEach((key) => {
|
||||
// list.push({
|
||||
// variable: key,
|
||||
// ...value[key],
|
||||
// })
|
||||
// })
|
||||
// })()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
|
|
@ -54,6 +43,14 @@ const InputVarList: FC<Props> = ({
|
|||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
const paramType = (type: string) => {
|
||||
if (type === FormTypeEnum.textNumber)
|
||||
return 'Number'
|
||||
else if (type === FormTypeEnum.files)
|
||||
return 'Files'
|
||||
else
|
||||
return 'String'
|
||||
}
|
||||
|
||||
const handleNotMixedTypeChange = useCallback((variable: string) => {
|
||||
return (varValue: ValueSelector | string, varKindType: VarKindType) => {
|
||||
|
|
@ -125,16 +122,18 @@ const InputVarList: FC<Props> = ({
|
|||
tooltip,
|
||||
}, index) => {
|
||||
const varInput = value[variable]
|
||||
const isString = type !== FormTypeEnum.textNumber
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isFile = type === FormTypeEnum.files
|
||||
const isString = type !== FormTypeEnum.textNumber && type !== FormTypeEnum.files
|
||||
return (
|
||||
<div key={variable} className='space-y-1'>
|
||||
<div className='flex items-center h-[18px] space-x-2'>
|
||||
<span className='text-[13px] font-medium text-gray-900'>{label[language] || label.en_US}</span>
|
||||
<span className='text-xs font-normal text-gray-500'>{!isString ? 'Number' : 'String'}</span>
|
||||
<span className='text-xs font-normal text-gray-500'>{paramType(type)}</span>
|
||||
{required && <span className='leading-[18px] text-xs font-normal text-[#EC4A0A]'>Required</span>}
|
||||
</div>
|
||||
{isString
|
||||
? (<Input
|
||||
{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)}
|
||||
|
|
@ -144,21 +143,33 @@ const InputVarList: FC<Props> = ({
|
|||
onFocusChange={handleInputFocus(variable)}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>)
|
||||
: (
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.type === VarKindType.constant ? (varInput?.value || '') : (varInput?.value || [])}
|
||||
onChange={handleNotMixedTypeChange(variable)}
|
||||
onOpen={handleOpen(index)}
|
||||
isSupportConstantValue={isSupportConstantValue}
|
||||
defaultVarKindType={varInput?.type}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
)}
|
||||
|
||||
/>
|
||||
)}
|
||||
{isNumber && (
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.type === VarKindType.constant ? (varInput?.value || '') : (varInput?.value || [])}
|
||||
onChange={handleNotMixedTypeChange(variable)}
|
||||
onOpen={handleOpen(index)}
|
||||
isSupportConstantValue={isSupportConstantValue}
|
||||
defaultVarKindType={varInput?.type}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
)}
|
||||
{isFile && (
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.type === VarKindType.constant ? (varInput?.value || '') : (varInput?.value || [])}
|
||||
onChange={handleNotMixedTypeChange(variable)}
|
||||
onOpen={handleOpen(index)}
|
||||
defaultVarKindType={VarKindType.variable}
|
||||
filterVar={(varPayload: Var) => varPayload.type === VarType.arrayFile}
|
||||
/>
|
||||
)}
|
||||
{tooltip && <div className='leading-[18px] text-xs font-normal text-gray-600'>{tooltip[language] || tooltip.en_US}</div>}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { CollectionType } from '@/app/components/tools/types'
|
||||
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
export enum VarType {
|
||||
|
|
@ -13,7 +14,7 @@ export type ToolVarInputs = Record<string, {
|
|||
|
||||
export type ToolNodeType = CommonNodeType & {
|
||||
provider_id: string
|
||||
provider_type: 'builtin'
|
||||
provider_type: CollectionType
|
||||
provider_name: string
|
||||
tool_name: string
|
||||
tool_label: string
|
||||
|
|
|
|||
|
|
@ -34,7 +34,20 @@ const useConfig = (id: string, payload: ToolNodeType) => {
|
|||
const isBuiltIn = provider_type === CollectionType.builtIn
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const currentTools = isBuiltIn ? buildInTools : customTools
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
|
||||
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)
|
||||
|
||||
// Auth
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useVariableAssigner } from '../../hooks'
|
||||
import type { VariableAssignerNodeType } from '../../types'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { Plus02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import AddVariablePopup from '@/app/components/workflow/nodes/_base/components/add-variable-popup'
|
||||
import type {
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
export type AddVariableProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
variableAssignerNodeId: string
|
||||
variableAssignerNodeData: VariableAssignerNodeType
|
||||
availableVars: NodeOutPutVar[]
|
||||
handleId?: string
|
||||
}
|
||||
const AddVariable = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
availableVars,
|
||||
variableAssignerNodeId,
|
||||
variableAssignerNodeData,
|
||||
handleId,
|
||||
}: AddVariableProps) => {
|
||||
const { handleAssignVariableValueChange } = useVariableAssigner()
|
||||
|
||||
const handleSelectVariable = useCallback((v: ValueSelector, varDetail: Var) => {
|
||||
handleAssignVariableValueChange(
|
||||
variableAssignerNodeId,
|
||||
v,
|
||||
varDetail,
|
||||
handleId,
|
||||
)
|
||||
onOpenChange(false)
|
||||
}, [handleAssignVariableValueChange, variableAssignerNodeId, handleId, onOpenChange])
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'hidden group-hover:flex absolute top-0 left-0 z-10 pointer-events-none',
|
||||
open && '!flex',
|
||||
variableAssignerNodeData.selected && '!flex',
|
||||
)}>
|
||||
<PortalToFollowElem
|
||||
placement={'left-start'}
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -60,
|
||||
}}
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => onOpenChange(!open)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center',
|
||||
'w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10',
|
||||
)}
|
||||
>
|
||||
<Plus02 className='w-2.5 h-2.5 text-white' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<AddVariablePopup
|
||||
onSelect={handleSelectVariable}
|
||||
availableVars={availableVars}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddVariable)
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useStore } from '../../../store'
|
||||
import { BlockEnum } from '../../../types'
|
||||
import type {
|
||||
Node,
|
||||
ValueSelector,
|
||||
VarType,
|
||||
} from '../../../types'
|
||||
import type { VariableAssignerNodeType } from '../types'
|
||||
import {
|
||||
useGetAvailableVars,
|
||||
useVariableAssigner,
|
||||
} from '../hooks'
|
||||
import { filterVar } from '../utils'
|
||||
import NodeHandle from './node-handle'
|
||||
import NodeVariableItem from './node-variable-item'
|
||||
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.variableAssigner'
|
||||
type GroupItem = {
|
||||
groupEnabled: boolean
|
||||
targetHandleId: string
|
||||
title: string
|
||||
type: string
|
||||
variables: ValueSelector[]
|
||||
variableAssignerNodeId: string
|
||||
variableAssignerNodeData: VariableAssignerNodeType
|
||||
}
|
||||
type NodeGroupItemProps = {
|
||||
item: GroupItem
|
||||
}
|
||||
const NodeGroupItem = ({
|
||||
item,
|
||||
}: NodeGroupItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const enteringNodePayload = useStore(s => s.enteringNodePayload)
|
||||
const hoveringAssignVariableGroupId = useStore(s => s.hoveringAssignVariableGroupId)
|
||||
const nodes: Node[] = useNodes()
|
||||
const {
|
||||
handleGroupItemMouseEnter,
|
||||
handleGroupItemMouseLeave,
|
||||
} = useVariableAssigner()
|
||||
const getAvailableVars = useGetAvailableVars()
|
||||
const outputType = useMemo(() => {
|
||||
if (item.targetHandleId === 'target')
|
||||
return item.variableAssignerNodeData.output_type
|
||||
|
||||
const group = item.variableAssignerNodeData.advanced_settings?.groups.find(group => group.groupId === item.targetHandleId)
|
||||
return group?.output_type || ''
|
||||
}, [item.variableAssignerNodeData, item.targetHandleId])
|
||||
const availableVars = getAvailableVars(item.variableAssignerNodeId, item.targetHandleId, filterVar(outputType as VarType))
|
||||
const showSelectionBorder = enteringNodePayload?.nodeId === item.variableAssignerNodeId && item.groupEnabled && hoveringAssignVariableGroupId === item.targetHandleId
|
||||
const connected = item.variableAssignerNodeData._connectedTargetHandleIds?.includes(item.targetHandleId)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative pt-1 px-1.5 pb-1.5 rounded-lg border border-transparent',
|
||||
showSelectionBorder && '!border-primary-600',
|
||||
)}
|
||||
onMouseEnter={() => handleGroupItemMouseEnter(item.targetHandleId)}
|
||||
onMouseLeave={handleGroupItemMouseLeave}
|
||||
>
|
||||
<div className='flex items-center justify-between h-4 text-[10px] font-medium text-gray-500'>
|
||||
<NodeHandle
|
||||
connected={connected}
|
||||
variableAssignerNodeId={item.variableAssignerNodeId}
|
||||
variableAssignerNodeData={item.variableAssignerNodeData}
|
||||
handleId={item.targetHandleId}
|
||||
availableVars={availableVars}
|
||||
/>
|
||||
<span className='grow uppercase truncate' title={item.title}>{item.title}</span>
|
||||
<span className='shrink-0 ml-2'>{item.type}</span>
|
||||
</div>
|
||||
{
|
||||
!item.variables.length && (
|
||||
<div className='relative flex items-center px-1 h-[22px] justify-between bg-gray-100 rounded-md space-x-1 text-[10px] font-normal text-gray-400 uppercase'>
|
||||
{t(`${i18nPrefix}.varNotSet`)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!item.variables.length && item.variables.map((variable = [], index) => {
|
||||
const isSystem = isSystemVar(variable)
|
||||
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
|
||||
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
|
||||
|
||||
return (
|
||||
<NodeVariableItem
|
||||
key={index}
|
||||
node={node as Node}
|
||||
varName={varName}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeGroupItem)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import type { MouseEvent } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
} from 'reactflow'
|
||||
import type { VariableAssignerNodeType } from '../types'
|
||||
import AddVariable from './add-variable'
|
||||
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
type NodeHandleProps = {
|
||||
handleId?: string
|
||||
connected?: boolean
|
||||
variableAssignerNodeId: string
|
||||
availableVars: NodeOutPutVar[]
|
||||
variableAssignerNodeData: VariableAssignerNodeType
|
||||
}
|
||||
const NodeHandle = ({
|
||||
connected,
|
||||
variableAssignerNodeId,
|
||||
handleId = 'target',
|
||||
availableVars,
|
||||
variableAssignerNodeData,
|
||||
}: NodeHandleProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const connectingNodePayload = useStore(s => s.connectingNodePayload)
|
||||
const isUnConnectable = connectingNodePayload?.handleType === 'source'
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
|
||||
const handleHandleClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setOpen(v => !v)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Handle
|
||||
id={handleId}
|
||||
type='target'
|
||||
onClick={handleHandleClick}
|
||||
position={Position.Left}
|
||||
isConnectable={!isUnConnectable}
|
||||
className={cn(
|
||||
'!-left-[13px] !top-1 !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1] !transform-none',
|
||||
'after:absolute after:w-0.5 after:h-2 after:left-[5px] after:top-1 after:bg-primary-500 pointer-events-none',
|
||||
!connected && 'after:opacity-0',
|
||||
)}
|
||||
>
|
||||
<AddVariable
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
variableAssignerNodeId={variableAssignerNodeId}
|
||||
variableAssignerNodeData={variableAssignerNodeData}
|
||||
handleId={handleId}
|
||||
availableVars={availableVars}
|
||||
/>
|
||||
</Handle>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeHandle)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { memo } from 'react'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type NodeVariableItemProps = {
|
||||
node: Node
|
||||
varName: string
|
||||
}
|
||||
const NodeVariableItem = ({
|
||||
node,
|
||||
varName,
|
||||
}: NodeVariableItemProps) => {
|
||||
return (
|
||||
<div className='relative flex items-center mt-0.5 h-6 bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700' >
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.data.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-[85px] truncate mx-0.5 text-xs font-medium text-gray-700' title={node?.data.title}>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='max-w-[75px] truncate ml-0.5 text-xs font-medium' title={varName}>{varName}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NodeVariableItem)
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
'use client'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { ChangeEvent, FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { VarGroupItem as VarGroupItemType } from '../types'
|
||||
import VarReferencePicker from '../../_base/components/variable/var-reference-picker'
|
||||
import VarList from '../components/var-list'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Folder } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.variableAssigner'
|
||||
|
||||
type Payload = VarGroupItemType & {
|
||||
group_name?: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
nodeId: string
|
||||
payload: Payload
|
||||
onChange: (newPayload: Payload) => void
|
||||
groupEnabled: boolean
|
||||
onGroupNameChange?: (value: string) => void
|
||||
canRemove?: boolean
|
||||
onRemove?: () => void
|
||||
availableVars: NodeOutPutVar[]
|
||||
}
|
||||
|
||||
const VarGroupItem: FC<Props> = ({
|
||||
readOnly,
|
||||
nodeId,
|
||||
payload,
|
||||
onChange,
|
||||
groupEnabled,
|
||||
onGroupNameChange,
|
||||
canRemove,
|
||||
onRemove,
|
||||
availableVars,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleAddVariable = useCallback((value: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
|
||||
const chosenVariables = payload.variables
|
||||
if (chosenVariables.some(item => item.join('.') === (value as ValueSelector).join('.')))
|
||||
return
|
||||
|
||||
const newPayload = produce(payload, (draft: Payload) => {
|
||||
draft.variables.push(value as ValueSelector)
|
||||
if (varInfo && varInfo.type !== VarType.any)
|
||||
draft.output_type = varInfo.type
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleListChange = useCallback((newList: ValueSelector[], changedItem?: ValueSelector) => {
|
||||
if (changedItem) {
|
||||
const chosenVariables = payload.variables
|
||||
if (chosenVariables.some(item => item.join('.') === (changedItem as ValueSelector).join('.')))
|
||||
return
|
||||
}
|
||||
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.variables = newList
|
||||
if (newList.length === 0)
|
||||
draft.output_type = VarType.any
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
if (payload.output_type === VarType.any)
|
||||
return true
|
||||
return varPayload.type === payload.output_type
|
||||
}, [payload.output_type])
|
||||
|
||||
const [isEditGroupName, {
|
||||
setTrue: setEditGroupName,
|
||||
setFalse: setNotEditGroupName,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleGroupNameChange = useCallback((e: ChangeEvent<any>) => {
|
||||
const value = e.target.value
|
||||
const { isValid, errorKey, errorMessageKey } = checkKeys([value], false)
|
||||
if (!isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
|
||||
})
|
||||
return
|
||||
}
|
||||
onGroupNameChange?.(value)
|
||||
}, [onGroupNameChange, t])
|
||||
|
||||
return (
|
||||
<Field
|
||||
className='group'
|
||||
title={groupEnabled
|
||||
? <div className='flex items-center'>
|
||||
<div className='flex items-center !normal-case'>
|
||||
<Folder className='mr-0.5 w-3.5 h-3.5' />
|
||||
{(!isEditGroupName)
|
||||
? (
|
||||
<div className='flex items-center h-6 px-1 rounded-lg cursor-text hover:bg-gray-100' onClick={setEditGroupName}>
|
||||
{payload.group_name}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<input
|
||||
type='text'
|
||||
className='h-6 px-1 rounded-lg bg-white border border-gray-300 focus:outline-none'
|
||||
// style={{
|
||||
// width: `${((payload.group_name?.length || 0) + 1) / 2}em`,
|
||||
// }}
|
||||
size={payload.group_name?.length} // to fit the input width
|
||||
autoFocus
|
||||
value={payload.group_name}
|
||||
onChange={handleGroupNameChange}
|
||||
onBlur={setNotEditGroupName}
|
||||
maxLength={30}
|
||||
/>)}
|
||||
|
||||
</div>
|
||||
{canRemove && (
|
||||
<div
|
||||
className='group-hover:block hidden ml-0.5 p-1 rounded-md text-gray-500 cursor-pointer hover:bg-[#FEE4E2] hover:text-[#D92D20]'
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Trash03
|
||||
className='w-4 h-4'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
: t(`${i18nPrefix}.title`)!}
|
||||
operations={
|
||||
<div className='flex items-center h-6 space-x-2'>
|
||||
{payload.variables.length > 0 && (
|
||||
<div className='flex items-center h-[18px] px-1 border border-black/8 rounded-[5px] text-xs font-medium text-gray-500 capitalize'>{payload.output_type}</div>
|
||||
)}
|
||||
{
|
||||
!readOnly
|
||||
? <VarReferencePicker
|
||||
isAddBtnTrigger
|
||||
readonly={false}
|
||||
nodeId={nodeId}
|
||||
isShowNodeName
|
||||
value={[]}
|
||||
onChange={handleAddVariable}
|
||||
defaultVarKindType={VarKindType.variable}
|
||||
filterVar={filterVar}
|
||||
availableVars={availableVars}
|
||||
/>
|
||||
: undefined
|
||||
}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<VarList
|
||||
readonly={readOnly}
|
||||
nodeId={nodeId}
|
||||
list={payload.variables}
|
||||
onChange={handleListChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
export default React.memo(VarGroupItem)
|
||||
|
|
@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import RemoveButton from '../../../_base/components/remove-button'
|
||||
import ListNoDataPlaceholder from '../../../_base/components/list-no-data-placeholder'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
|
|
@ -12,9 +13,8 @@ type Props = {
|
|||
readonly: boolean
|
||||
nodeId: string
|
||||
list: ValueSelector[]
|
||||
onChange: (list: ValueSelector[]) => void
|
||||
onChange: (list: ValueSelector[], value?: ValueSelector) => void
|
||||
onOpen?: (index: number) => void
|
||||
onlyLeafNodeVar?: boolean
|
||||
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +24,6 @@ const VarList: FC<Props> = ({
|
|||
list,
|
||||
onChange,
|
||||
onOpen = () => { },
|
||||
onlyLeafNodeVar,
|
||||
filterVar,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -33,7 +32,7 @@ const VarList: FC<Props> = ({
|
|||
const newList = produce(list, (draft) => {
|
||||
draft[index] = value as ValueSelector
|
||||
})
|
||||
onChange(newList)
|
||||
onChange(newList, value as ValueSelector)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
|
|
@ -52,9 +51,9 @@ const VarList: FC<Props> = ({
|
|||
|
||||
if (list.length === 0) {
|
||||
return (
|
||||
<div className='flex rounded-md bg-gray-50 items-center h-[42px] justify-center leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<ListNoDataPlaceholder>
|
||||
{t('workflow.nodes.variableAssigner.noVarTip')}
|
||||
</div>
|
||||
</ListNoDataPlaceholder>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +69,6 @@ const VarList: FC<Props> = ({
|
|||
value={item}
|
||||
onChange={handleVarReferenceChange(index)}
|
||||
onOpen={handleOpen(index)}
|
||||
onlyLeafNodeVar={onlyLeafNodeVar}
|
||||
filterVar={filterVar}
|
||||
defaultVarKindType={VarKindType.variable}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { useCallback } from 'react'
|
|||
import produce from 'immer'
|
||||
import type { VariableAssignerNodeType } from '../../types'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import { useEdgesInteractions } from '@/app/components/workflow/hooks'
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
|
|
@ -10,18 +9,15 @@ type Params = {
|
|||
setInputs: (newInputs: VariableAssignerNodeType) => void
|
||||
}
|
||||
function useVarList({
|
||||
id,
|
||||
inputs,
|
||||
setInputs,
|
||||
}: Params) {
|
||||
const { handleVariableAssignerEdgesChange } = useEdgesInteractions()
|
||||
const handleVarListChange = useCallback((newList: ValueSelector[]) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.variables = newList
|
||||
})
|
||||
setInputs(newInputs)
|
||||
handleVariableAssignerEdgesChange(id, newList)
|
||||
}, [inputs, setInputs, id, handleVariableAssignerEdgesChange])
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleAddVariable = useCallback(() => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ const i18nPrefix = 'workflow'
|
|||
|
||||
const nodeDefault: NodeDefault<VariableAssignerNodeType> = {
|
||||
defaultValue: {
|
||||
output_type: VarType.string,
|
||||
output_type: VarType.any,
|
||||
variables: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes.filter(type => type !== BlockEnum.IfElse && type !== BlockEnum.QuestionClassifier)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
|
|
|
|||
243
web/app/components/workflow/nodes/variable-assigner/hooks.ts
Normal file
243
web/app/components/workflow/nodes/variable-assigner/hooks.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { useCallback } from 'react'
|
||||
import {
|
||||
useEdges,
|
||||
useNodes,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodeDataUpdate,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../../utils'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '../../types'
|
||||
import { useWorkflowStore } from '../../store'
|
||||
import type {
|
||||
VarGroupItem,
|
||||
VariableAssignerNodeType,
|
||||
} from './types'
|
||||
import { toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
export const useVariableAssigner = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
|
||||
const handleAssignVariableValueChange = useCallback((nodeId: string, value: ValueSelector, varDetail: Var, groupId?: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const node: Node<VariableAssignerNodeType> = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
let payload
|
||||
if (groupId && groupId !== 'target') {
|
||||
payload = {
|
||||
advanced_settings: {
|
||||
...node.data.advanced_settings,
|
||||
groups: node.data.advanced_settings?.groups.map((group: VarGroupItem & { groupId: string }) => {
|
||||
if (group.groupId === groupId && !group.variables.some(item => item.join('.') === (value as ValueSelector).join('.'))) {
|
||||
return {
|
||||
...group,
|
||||
variables: [...group.variables, value],
|
||||
output_type: varDetail.type,
|
||||
}
|
||||
}
|
||||
return group
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (node.data.variables.some(item => item.join('.') === (value as ValueSelector).join('.')))
|
||||
return
|
||||
payload = {
|
||||
variables: [...node.data.variables, value],
|
||||
output_type: varDetail.type,
|
||||
}
|
||||
}
|
||||
handleNodeDataUpdate({
|
||||
id: nodeId,
|
||||
data: payload,
|
||||
})
|
||||
}, [store, handleNodeDataUpdate])
|
||||
|
||||
const handleAddVariableInAddVariablePopupWithPosition = useCallback((
|
||||
nodeId: string,
|
||||
variableAssignerNodeId: string,
|
||||
variableAssignerNodeHandleId: string,
|
||||
value: ValueSelector,
|
||||
varDetail: Var,
|
||||
) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const {
|
||||
setShowAssignVariablePopup,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.id === nodeId || node.id === variableAssignerNodeId) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
_showAddVariablePopup: false,
|
||||
_holdAddVariablePopup: false,
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
setShowAssignVariablePopup(undefined)
|
||||
handleAssignVariableValueChange(variableAssignerNodeId, value, varDetail, variableAssignerNodeHandleId)
|
||||
}, [store, workflowStore, handleAssignVariableValueChange])
|
||||
|
||||
const handleRemoveEdges = useCallback((nodeId: string, enabled: boolean) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const needDeleteEdges = edges.filter(edge => edge.target === nodeId)
|
||||
|
||||
if (!needDeleteEdges.length)
|
||||
return
|
||||
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
const groups = currentNode.data.advanced_settings?.groups || []
|
||||
|
||||
let shouldKeepEdges: Edge[] = []
|
||||
|
||||
if (enabled) {
|
||||
shouldKeepEdges = edges.filter((edge) => {
|
||||
return edge.target === nodeId && edge.targetHandle === 'target'
|
||||
}).map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
targetHandle: groups[0].groupId,
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
shouldKeepEdges = edges.filter((edge) => {
|
||||
return edge.target === nodeId && edge.targetHandle === groups[0].groupId
|
||||
}).map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
targetHandle: 'target',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
[
|
||||
...needDeleteEdges.map((needDeleteEdge) => {
|
||||
return {
|
||||
type: 'remove',
|
||||
edge: needDeleteEdge,
|
||||
}
|
||||
}),
|
||||
...shouldKeepEdges.map((shouldKeepEdge) => {
|
||||
return {
|
||||
type: 'add',
|
||||
edge: shouldKeepEdge,
|
||||
}
|
||||
}),
|
||||
],
|
||||
nodes,
|
||||
)
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft = draft.filter(edge => edge.target !== nodeId)
|
||||
draft.push(...shouldKeepEdges)
|
||||
return draft
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
const handleGroupItemMouseEnter = useCallback((groupId: string) => {
|
||||
const {
|
||||
setHoveringAssignVariableGroupId,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setHoveringAssignVariableGroupId(groupId)
|
||||
}, [workflowStore])
|
||||
|
||||
const handleGroupItemMouseLeave = useCallback(() => {
|
||||
const {
|
||||
connectingNodePayload,
|
||||
setHoveringAssignVariableGroupId,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (connectingNodePayload)
|
||||
setHoveringAssignVariableGroupId(undefined)
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleAddVariableInAddVariablePopupWithPosition,
|
||||
handleRemoveEdges,
|
||||
handleGroupItemMouseEnter,
|
||||
handleGroupItemMouseLeave,
|
||||
handleAssignVariableValueChange,
|
||||
}
|
||||
}
|
||||
|
||||
export const useGetAvailableVars = () => {
|
||||
const { t } = useTranslation()
|
||||
const nodes: Node[] = useNodes()
|
||||
const edges: Edge[] = useEdges()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const isChatMode = useIsChatMode()
|
||||
const getAvailableVars = useCallback((nodeId: string, handleId: string, filterVar: (v: Var) => boolean) => {
|
||||
const availableNodes: Node[] = []
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
if (!currentNode)
|
||||
return []
|
||||
const parentNode = nodes.find(node => node.id === currentNode.parentId)
|
||||
const connectedEdges = edges.filter(edge => edge.target === nodeId && edge.targetHandle === handleId)
|
||||
|
||||
if (parentNode && !connectedEdges.length) {
|
||||
const beforeNodes = getBeforeNodesInSameBranch(parentNode.id)
|
||||
availableNodes.push(...beforeNodes)
|
||||
}
|
||||
else {
|
||||
connectedEdges.forEach((connectedEdge) => {
|
||||
const beforeNodes = getBeforeNodesInSameBranch(connectedEdge.source)
|
||||
const connectedNode = nodes.find(node => node.id === connectedEdge.source)!
|
||||
|
||||
availableNodes.push(connectedNode, ...beforeNodes)
|
||||
})
|
||||
}
|
||||
|
||||
return toNodeAvailableVars({
|
||||
parentNode,
|
||||
t,
|
||||
beforeNodes: uniqBy(availableNodes, 'id').filter(node => node.id !== nodeId),
|
||||
isChatMode,
|
||||
filterVar,
|
||||
})
|
||||
}, [nodes, edges, t, isChatMode, getBeforeNodesInSameBranch])
|
||||
|
||||
return getAvailableVars
|
||||
}
|
||||
|
|
@ -1,91 +1,61 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NodeTargetHandle } from '../_base/components/node-handle'
|
||||
import { BlockEnum } from '../../types'
|
||||
import NodeGroupItem from './components/node-group-item'
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import {
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.variableAssigner'
|
||||
|
||||
const Node: FC<NodeProps<VariableAssignerNodeType>> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const { id, data } = props
|
||||
const { variables: originVariables, output_type } = data
|
||||
const { getTreeLeafNodes } = useWorkflow()
|
||||
const { advanced_settings } = data
|
||||
|
||||
const availableNodes = getTreeLeafNodes(id)
|
||||
const variables = originVariables.filter(item => item.length > 0)
|
||||
const groups = useMemo(() => {
|
||||
if (!advanced_settings?.group_enabled) {
|
||||
return [{
|
||||
groupEnabled: false,
|
||||
targetHandleId: 'target',
|
||||
title: t(`${i18nPrefix}.title`),
|
||||
type: data.output_type,
|
||||
variables: data.variables,
|
||||
variableAssignerNodeId: id,
|
||||
variableAssignerNodeData: data,
|
||||
}]
|
||||
}
|
||||
return advanced_settings.groups.map((group) => {
|
||||
return {
|
||||
groupEnabled: true,
|
||||
targetHandleId: group.groupId,
|
||||
title: group.group_name,
|
||||
type: group.output_type,
|
||||
variables: group.variables,
|
||||
variableAssignerNodeId: id,
|
||||
variableAssignerNodeData: data,
|
||||
}
|
||||
})
|
||||
}, [t, advanced_settings, data, id])
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
<div className='mb-0.5 leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${i18nPrefix}.title`)}</div>
|
||||
<div className='relative mb-1 px-1' ref={ref}>
|
||||
{
|
||||
variables.length === 0 && (
|
||||
<div className='relative flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-400 uppercase'>
|
||||
{t(`${i18nPrefix}.varNotSet`)}
|
||||
<NodeTargetHandle
|
||||
{...props}
|
||||
handleId='varNotSet'
|
||||
handleClassName='!top-1/2 !-translate-y-1/2 !-left-[21px]'
|
||||
groups.map((item) => {
|
||||
return (
|
||||
<NodeGroupItem
|
||||
key={item.title}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{variables.length > 0 && (
|
||||
<>
|
||||
<div className='space-y-0.5'>
|
||||
{variables.map((item, index) => {
|
||||
const node = availableNodes.find(node => node.id === item[0])
|
||||
const varName = item[item.length - 1]
|
||||
|
||||
return (
|
||||
<div key={index} className='relative flex items-center h-6 bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700' >
|
||||
<NodeTargetHandle
|
||||
{...props}
|
||||
handleId={item[0]}
|
||||
handleClassName='!top-1/2 !-translate-y-1/2 !-left-[21px]'
|
||||
/>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={(node?.data.type as BlockEnum) || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-[85px] truncate mx-0.5 text-xs font-medium text-gray-700' title={node?.data.title}>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='max-w-[75px] truncate ml-0.5 text-xs font-medium' title={varName}>{varName}</div>
|
||||
</div>
|
||||
{/* <div className='ml-0.5 text-xs font-normal text-gray-500'>{output_type}</div> */}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-2 flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'>
|
||||
<div className='text-xs font-medium text-gray-500 uppercase'>
|
||||
{t(`${i18nPrefix}.outputType`)}
|
||||
</div>
|
||||
<div className='text-xs font-normal text-gray-700'>
|
||||
{t(`${i18nPrefix}.type.${output_type}`)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
export default memo(Node)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
// import cn from 'classnames'
|
||||
// import Field from '../_base/components/field'
|
||||
import RemoveEffectVarConfirm from '../_base/components/remove-effect-var-confirm'
|
||||
import useConfig from './use-config'
|
||||
import VarList from './components/var-list'
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Selector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import VarGroupItem from './components/var-group-item'
|
||||
import { type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
// import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
// import Switch from '@/app/components/base/switch'
|
||||
import AddButton from '@/app/components/workflow/nodes/_base/components/add-button'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.variableAssigner'
|
||||
const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
|
||||
|
|
@ -23,70 +23,105 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
|
|||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleOutputTypeChange,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
handleOnVarOpen,
|
||||
handleListOrTypeChange,
|
||||
isEnableGroup,
|
||||
// handleGroupEnabledChange,
|
||||
handleAddGroup,
|
||||
handleListOrTypeChangeInGroup,
|
||||
handleGroupRemoved,
|
||||
handleVarGroupNameChange,
|
||||
isShowRemoveVarConfirm,
|
||||
hideRemoveVarConfirm,
|
||||
onRemoveVarConfirm,
|
||||
getAvailableVars,
|
||||
filterVar,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const typeOptions = [
|
||||
{ label: t(`${i18nPrefix}.type.string`), value: VarType.string },
|
||||
{ label: t(`${i18nPrefix}.type.number`), value: VarType.number },
|
||||
{ label: t(`${i18nPrefix}.type.object`), value: VarType.object },
|
||||
{ label: t(`${i18nPrefix}.type.array`), value: VarType.array },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.outputVarType`)}
|
||||
>
|
||||
<Selector
|
||||
readonly={readOnly}
|
||||
value={inputs.output_type}
|
||||
options={typeOptions}
|
||||
onChange={handleOutputTypeChange}
|
||||
trigger={
|
||||
<div className='flex items-center h-8 justify-between px-2.5 rounded-lg bg-gray-100 capitalize'>
|
||||
<div className='text-[13px] font-normal text-gray-900'>{inputs.output_type}</div>
|
||||
{!readOnly && <ChevronDown className='w-3.5 h-3.5 text-gray-700' />}
|
||||
</div>
|
||||
}
|
||||
popupClassName='!top-[36px] !w-[387px]'
|
||||
showChecked
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.title`)}
|
||||
operations={
|
||||
!readOnly ? <AddButton onClick={handleAddVariable} /> : undefined
|
||||
}
|
||||
>
|
||||
<VarList
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
list={inputs.variables}
|
||||
onChange={handleVarListChange}
|
||||
onOpen={handleOnVarOpen}
|
||||
onlyLeafNodeVar
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='output'
|
||||
type={inputs.output_type}
|
||||
description={t(`${i18nPrefix}.outputVars.output`)}
|
||||
{!isEnableGroup
|
||||
? (
|
||||
<VarGroupItem
|
||||
readOnly={readOnly}
|
||||
nodeId={id}
|
||||
payload={{
|
||||
output_type: inputs.output_type,
|
||||
variables: inputs.variables,
|
||||
}}
|
||||
onChange={handleListOrTypeChange}
|
||||
groupEnabled={false}
|
||||
availableVars={getAvailableVars(id, 'target', filterVar(inputs.output_type))}
|
||||
/>
|
||||
</>
|
||||
</OutputVars>
|
||||
)
|
||||
: (<div>
|
||||
<div className='space-y-2'>
|
||||
{inputs.advanced_settings?.groups.map((item, index) => (
|
||||
<div key={item.groupId}>
|
||||
<VarGroupItem
|
||||
readOnly={readOnly}
|
||||
nodeId={id}
|
||||
payload={item}
|
||||
onChange={handleListOrTypeChangeInGroup(item.groupId)}
|
||||
groupEnabled
|
||||
canRemove={!readOnly && inputs.advanced_settings?.groups.length > 1}
|
||||
onRemove={handleGroupRemoved(item.groupId)}
|
||||
onGroupNameChange={handleVarGroupNameChange(item.groupId)}
|
||||
availableVars={getAvailableVars(id, item.groupId, filterVar(item.output_type))}
|
||||
/>
|
||||
{index !== inputs.advanced_settings?.groups.length - 1 && <Split className='my-4' />}
|
||||
</div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
<AddButton
|
||||
className='mt-2'
|
||||
text={t(`${i18nPrefix}.addGroup`)}
|
||||
onClick={handleAddGroup}
|
||||
/>
|
||||
</div>)}
|
||||
</div>
|
||||
{/* <Split /> */}
|
||||
{/* <div className={cn('px-4 pt-4', isEnableGroup ? 'pb-4' : 'pb-2')}>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.aggregationGroup`)}
|
||||
tooltip={t(`${i18nPrefix}.aggregationGroupTip`)!}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={isEnableGroup}
|
||||
onChange={handleGroupEnabledChange}
|
||||
size='md'
|
||||
disabled={readOnly}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div> */}
|
||||
{/* {isEnableGroup && (
|
||||
<>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
{inputs.advanced_settings?.groups.map((item, index) => (
|
||||
<VarItem
|
||||
key={index}
|
||||
name={`${item.group_name}.output`}
|
||||
type={item.output_type}
|
||||
description={t(`${i18nPrefix}.outputVars.varDescribe`, {
|
||||
groupName: item.group_name,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
</>
|
||||
)} */}
|
||||
<RemoveEffectVarConfirm
|
||||
isShow={isShowRemoveVarConfirm}
|
||||
onCancel={hideRemoveVarConfirm}
|
||||
onConfirm={onRemoveVarConfirm}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import type { CommonNodeType, ValueSelector, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export type VariableAssignerNodeType = CommonNodeType & {
|
||||
export type VarGroupItem = {
|
||||
output_type: VarType
|
||||
variables: ValueSelector[]
|
||||
}
|
||||
export type VariableAssignerNodeType = CommonNodeType & VarGroupItem & {
|
||||
advanced_settings: {
|
||||
group_enabled: boolean
|
||||
groups: ({
|
||||
group_name: string
|
||||
groupId: string
|
||||
} & VarGroupItem)[]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import useVarList from './components/var-list/use-var-list'
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import type { VarGroupItem, VariableAssignerNodeType } from './types'
|
||||
import { useGetAvailableVars, useVariableAssigner } from './hooks'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
|
|
@ -12,68 +15,176 @@ import {
|
|||
|
||||
const useConfig = (id: string, payload: VariableAssignerNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<VariableAssignerNodeType>(id, payload)
|
||||
const { getBeforeNodeById } = useWorkflow()
|
||||
const beforeNodes = getBeforeNodeById(id)
|
||||
const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()
|
||||
|
||||
useEffect(() => {
|
||||
if (beforeNodes.length !== 1 || inputs.variables.length > 0)
|
||||
return
|
||||
const beforeNode = beforeNodes[0]
|
||||
if (beforeNode.data.type === BlockEnum.KnowledgeRetrieval) {
|
||||
const newInputs = produce(inputs, (draft: VariableAssignerNodeType) => {
|
||||
draft.output_type = VarType.array
|
||||
draft.variables[0] = [beforeNode.id, 'result']
|
||||
const { inputs, setInputs } = useNodeCrud<VariableAssignerNodeType>(id, payload)
|
||||
const isEnableGroup = !!inputs.advanced_settings?.group_enabled
|
||||
const { handleRemoveEdges } = useVariableAssigner()
|
||||
|
||||
// Not Enable Group
|
||||
const handleListOrTypeChange = useCallback((payload: VarGroupItem) => {
|
||||
setInputs({
|
||||
...inputs,
|
||||
...payload,
|
||||
})
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleListOrTypeChangeInGroup = useCallback((groupId: string) => {
|
||||
return (payload: VarGroupItem) => {
|
||||
const index = inputs.advanced_settings.groups.findIndex(item => item.groupId === groupId)
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.advanced_settings.groups[index] = {
|
||||
...draft.advanced_settings.groups[index],
|
||||
...payload,
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [beforeNodes, inputs.variables])
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleOutputTypeChange = useCallback((outputType: string) => {
|
||||
const newInputs = produce(inputs, (draft: VariableAssignerNodeType) => {
|
||||
draft.output_type = outputType as VarType
|
||||
const getAvailableVars = useGetAvailableVars()
|
||||
const filterVar = (varType: VarType) => {
|
||||
return (v: Var) => {
|
||||
if (varType === VarType.any)
|
||||
return true
|
||||
if (v.type === VarType.any)
|
||||
return true
|
||||
return v.type === varType
|
||||
}
|
||||
}
|
||||
|
||||
const [isShowRemoveVarConfirm, {
|
||||
setTrue: showRemoveVarConfirm,
|
||||
setFalse: hideRemoveVarConfirm,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [removedVars, setRemovedVars] = useState<ValueSelector[]>([])
|
||||
const [removeType, setRemoveType] = useState<'group' | 'enableChanged'>('group')
|
||||
const [removedGroupIndex, setRemovedGroupIndex] = useState<number>(-1)
|
||||
const handleGroupRemoved = useCallback((groupId: string) => {
|
||||
return () => {
|
||||
const index = inputs.advanced_settings.groups.findIndex(item => item.groupId === groupId)
|
||||
if (isVarUsedInNodes([id, inputs.advanced_settings.groups[index].group_name, 'output'])) {
|
||||
showRemoveVarConfirm()
|
||||
setRemovedVars([[id, inputs.advanced_settings.groups[index].group_name, 'output']])
|
||||
setRemoveType('group')
|
||||
setRemovedGroupIndex(index)
|
||||
return
|
||||
}
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.advanced_settings.groups.splice(index, 1)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
}, [id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm])
|
||||
|
||||
const handleGroupEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
if (!draft.advanced_settings)
|
||||
draft.advanced_settings = { group_enabled: false, groups: [] }
|
||||
if (enabled) {
|
||||
if (draft.advanced_settings.groups.length === 0) {
|
||||
const DEFAULT_GROUP_NAME = 'Group1'
|
||||
draft.advanced_settings.groups = [{
|
||||
output_type: draft.output_type,
|
||||
variables: draft.variables,
|
||||
group_name: DEFAULT_GROUP_NAME,
|
||||
groupId: uuid4(),
|
||||
}]
|
||||
|
||||
handleOutVarRenameChange(id, [id, 'output'], [id, DEFAULT_GROUP_NAME, 'output'])
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (draft.advanced_settings.groups.length > 0) {
|
||||
if (draft.advanced_settings.groups.length > 1) {
|
||||
const useVars = draft.advanced_settings.groups.filter((item, index) => index > 0 && isVarUsedInNodes([id, item.group_name, 'output']))
|
||||
if (useVars.length > 0) {
|
||||
showRemoveVarConfirm()
|
||||
setRemovedVars(useVars.map(item => [id, item.group_name, 'output']))
|
||||
setRemoveType('enableChanged')
|
||||
return
|
||||
}
|
||||
}
|
||||
draft.output_type = draft.advanced_settings.groups[0].output_type
|
||||
draft.variables = draft.advanced_settings.groups[0].variables
|
||||
handleOutVarRenameChange(id, [id, draft.advanced_settings.groups[0].group_name, 'output'], [id, 'output'])
|
||||
}
|
||||
}
|
||||
draft.advanced_settings.group_enabled = enabled
|
||||
})
|
||||
setInputs(newInputs)
|
||||
handleRemoveEdges(id, enabled)
|
||||
}, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm, handleRemoveEdges])
|
||||
|
||||
const handleAddGroup = useCallback(() => {
|
||||
let maxInGroupName = 1
|
||||
inputs.advanced_settings.groups.forEach((item) => {
|
||||
const match = item.group_name.match(/(\d+)$/)
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10)
|
||||
if (num > maxInGroupName)
|
||||
maxInGroupName = num
|
||||
}
|
||||
})
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.advanced_settings.groups.push({
|
||||
output_type: VarType.any,
|
||||
variables: [],
|
||||
group_name: `Group${maxInGroupName + 1}`,
|
||||
groupId: uuid4(),
|
||||
})
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const { handleVarListChange, handleAddVariable } = useVarList({
|
||||
id,
|
||||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
const handleVarGroupNameChange = useCallback((groupId: string) => {
|
||||
return (name: string) => {
|
||||
const index = inputs.advanced_settings.groups.findIndex(item => item.groupId === groupId)
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.advanced_settings.groups[index].group_name = name
|
||||
})
|
||||
handleOutVarRenameChange(id, [id, inputs.advanced_settings.groups[index].group_name, 'output'], [id, name, 'output'])
|
||||
setInputs(newInputs)
|
||||
}
|
||||
}, [handleOutVarRenameChange, id, inputs, setInputs])
|
||||
|
||||
const { variables } = inputs
|
||||
const [currVarIndex, setCurrVarIndex] = useState(-1)
|
||||
const currVar = variables[currVarIndex]
|
||||
const handleOnVarOpen = useCallback((index: number) => {
|
||||
setCurrVarIndex(index)
|
||||
}, [])
|
||||
const filterVar = useCallback((varPayload: Var, valueSelector: ValueSelector) => {
|
||||
const type = varPayload.type
|
||||
if ((inputs.output_type !== VarType.array && type !== inputs.output_type) || (
|
||||
inputs.output_type === VarType.array && ![VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(type)
|
||||
))
|
||||
return false
|
||||
const onRemoveVarConfirm = useCallback(() => {
|
||||
removedVars.forEach((v) => {
|
||||
removeUsedVarInNodes(v)
|
||||
})
|
||||
hideRemoveVarConfirm()
|
||||
if (removeType === 'group') {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.advanced_settings.groups.splice(removedGroupIndex, 1)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
else {
|
||||
// removeType === 'enableChanged' to enabled
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.advanced_settings.group_enabled = false
|
||||
draft.output_type = draft.advanced_settings.groups[0].output_type
|
||||
draft.variables = draft.advanced_settings.groups[0].variables
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
}, [removedVars, hideRemoveVarConfirm, removeType, removeUsedVarInNodes, inputs, setInputs, removedGroupIndex])
|
||||
|
||||
// can not choose the same node
|
||||
if (!currVar)
|
||||
return true
|
||||
|
||||
const selectNodeId = valueSelector[0]
|
||||
|
||||
if (selectNodeId !== currVar[0] && variables.find(v => v[0] === selectNodeId))
|
||||
return false
|
||||
|
||||
return true
|
||||
}, [currVar, inputs.output_type, variables])
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleOutputTypeChange,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
handleOnVarOpen,
|
||||
handleListOrTypeChange,
|
||||
isEnableGroup,
|
||||
handleGroupEnabledChange,
|
||||
handleAddGroup,
|
||||
handleListOrTypeChangeInGroup,
|
||||
handleGroupRemoved,
|
||||
handleVarGroupNameChange,
|
||||
isShowRemoveVarConfirm,
|
||||
hideRemoveVarConfirm,
|
||||
onRemoveVarConfirm,
|
||||
getAvailableVars,
|
||||
filterVar,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
import type { VariableAssignerNodeType } from './types'
|
||||
import type { Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
|
||||
export const checkNodeValid = (payload: VariableAssignerNodeType) => {
|
||||
export const checkNodeValid = () => {
|
||||
return true
|
||||
}
|
||||
|
||||
export const filterVar = (varType: VarType) => {
|
||||
return (v: Var) => {
|
||||
if (varType === VarType.any)
|
||||
return true
|
||||
if (v.type === VarType.any)
|
||||
return true
|
||||
return v.type === varType
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue