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:
zxhlyh
2024-05-27 21:57:08 +08:00
committed by GitHub
parent 444fdb79dc
commit 45deaee762
210 changed files with 9951 additions and 2223 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -46,6 +46,9 @@ const Base: FC<Props> = ({
const handleCopy = useCallback(() => {
copy(value)
setIsCopied(true)
setTimeout(() => {
setIsCopied(false)
}, 2000)
}, [value])
return (

View File

@@ -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'>

View File

@@ -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)

View File

@@ -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)}
/>
)
}

View File

@@ -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}
/>
)
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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'}
/>
)

View File

@@ -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
})

View File

@@ -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

View File

@@ -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>

View File

@@ -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> = ({