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

@ -8,8 +8,10 @@ import {
Home,
Http,
IfElse,
Iteration,
KnowledgeRetrieval,
Llm,
ParameterExtractor,
QuestionClassifier,
TemplatingTransform,
VariableX,
@ -40,7 +42,10 @@ const getIcon = (type: BlockEnum, className: string) => {
[BlockEnum.QuestionClassifier]: <QuestionClassifier className={className} />,
[BlockEnum.TemplateTransform]: <TemplatingTransform className={className} />,
[BlockEnum.VariableAssigner]: <VariableX className={className} />,
[BlockEnum.VariableAggregator]: <VariableX className={className} />,
[BlockEnum.Tool]: <VariableX className={className} />,
[BlockEnum.Iteration]: <Iteration className={className} />,
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
}[type]
}
const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
@ -49,12 +54,15 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
[BlockEnum.Code]: 'bg-[#2E90FA]',
[BlockEnum.End]: 'bg-[#F79009]',
[BlockEnum.IfElse]: 'bg-[#06AED4]',
[BlockEnum.Iteration]: 'bg-[#06AED4]',
[BlockEnum.HttpRequest]: 'bg-[#875BF7]',
[BlockEnum.Answer]: 'bg-[#F79009]',
[BlockEnum.KnowledgeRetrieval]: 'bg-[#16B364]',
[BlockEnum.QuestionClassifier]: 'bg-[#16B364]',
[BlockEnum.TemplateTransform]: 'bg-[#2E90FA]',
[BlockEnum.VariableAssigner]: 'bg-[#2E90FA]',
[BlockEnum.VariableAggregator]: 'bg-[#2E90FA]',
[BlockEnum.ParameterExtractor]: 'bg-[#2E90FA]',
}
const BlockIcon: FC<BlockIconProps> = ({
type,

View File

@ -0,0 +1,76 @@
import {
useMemo,
useState,
} from 'react'
import cn from 'classnames'
import type {
OnSelectBlock,
ToolWithProvider,
} from '../types'
import { useStore } from '../store'
import { ToolTypeEnum } from './types'
import Tools from './tools'
import { useToolTabs } from './hooks'
import { useGetLanguage } from '@/context/i18n'
type AllToolsProps = {
searchText: string
onSelect: OnSelectBlock
}
const AllTools = ({
searchText,
onSelect,
}: AllToolsProps) => {
const language = useGetLanguage()
const tabs = useToolTabs()
const [activeTab, setActiveTab] = useState(ToolTypeEnum.All)
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const tools = useMemo(() => {
let mergedTools: ToolWithProvider[] = []
if (activeTab === ToolTypeEnum.All)
mergedTools = [...buildInTools, ...customTools]
if (activeTab === ToolTypeEnum.BuiltIn)
mergedTools = buildInTools
if (activeTab === ToolTypeEnum.Custom)
mergedTools = customTools
if (activeTab === ToolTypeEnum.Workflow)
mergedTools = workflowTools
return mergedTools.filter((toolWithProvider) => {
return toolWithProvider.tools.some((tool) => {
return tool.label[language].toLowerCase().includes(searchText.toLowerCase())
})
})
}, [activeTab, buildInTools, customTools, workflowTools, searchText, language])
return (
<div>
<div className='flex items-center px-3 h-8 space-x-1 bg-gray-25 border-b-[0.5px] border-black/[0.08] shadow-xs'>
{
tabs.map(tab => (
<div
className={cn(
'flex items-center px-2 h-6 rounded-md hover:bg-gray-100 cursor-pointer',
'text-xs font-medium text-gray-700',
activeTab === tab.key && 'bg-gray-200',
)}
key={tab.key}
onClick={() => setActiveTab(tab.key)}
>
{tab.name}
</div>
))
}
</div>
<Tools
showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow}
tools={tools}
onSelect={onSelect}
/>
</div>
)
}
export default AllTools

View File

@ -39,6 +39,11 @@ export const BLOCKS: Block[] = [
type: BlockEnum.IfElse,
title: 'IF/ELSE',
},
{
classification: BlockClassificationEnum.Logic,
type: BlockEnum.Iteration,
title: 'Iteration',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.Code,
@ -51,8 +56,13 @@ export const BLOCKS: Block[] = [
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.VariableAssigner,
title: 'Variable Assigner',
type: BlockEnum.VariableAggregator,
title: 'Variable Aggregator',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.ParameterExtractor,
title: 'Parameter Extractor',
},
{
classification: BlockClassificationEnum.Utilities,

View File

@ -1,6 +1,9 @@
import { useTranslation } from 'react-i18next'
import { BLOCKS } from './constants'
import { TabsEnum } from './types'
import {
TabsEnum,
ToolTypeEnum,
} from './types'
export const useBlocks = () => {
const { t } = useTranslation()
@ -22,12 +25,31 @@ export const useTabs = () => {
name: t('workflow.tabs.blocks'),
},
{
key: TabsEnum.BuiltInTool,
name: t('workflow.tabs.builtInTool'),
},
{
key: TabsEnum.CustomTool,
name: t('workflow.tabs.customTool'),
key: TabsEnum.Tools,
name: t('workflow.tabs.tools'),
},
]
}
export const useToolTabs = () => {
const { t } = useTranslation()
return [
{
key: ToolTypeEnum.All,
name: t('workflow.tabs.allTool'),
},
{
key: ToolTypeEnum.BuiltIn,
name: t('workflow.tabs.builtInTool'),
},
{
key: ToolTypeEnum.Custom,
name: t('workflow.tabs.customTool'),
},
{
key: ToolTypeEnum.Workflow,
name: t('workflow.tabs.workflowTool'),
},
]
}

View File

@ -39,6 +39,7 @@ type NodeSelectorProps = {
asChild?: boolean
availableBlocksTypes?: BlockEnum[]
disabled?: boolean
noBlocks?: boolean
}
const NodeSelector: FC<NodeSelectorProps> = ({
open: openFromProps,
@ -54,6 +55,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
asChild,
availableBlocksTypes,
disabled,
noBlocks = false,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
@ -136,6 +138,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
onSelect={handleSelect}
searchText={searchText}
availableBlocksTypes={availableBlocksTypes}
noBlocks={noBlocks}
/>
</div>
</PortalToFollowElemContent>

View File

@ -3,48 +3,55 @@ import {
memo,
useState,
} from 'react'
import cn from 'classnames'
import type { BlockEnum } from '../types'
import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
import { TabsEnum } from './types'
import Tools from './tools'
import Blocks from './blocks'
import AllTools from './all-tools'
export type TabsProps = {
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
noBlocks?: boolean
}
const Tabs: FC<TabsProps> = ({
searchText,
onSelect,
availableBlocksTypes,
noBlocks,
}) => {
const tabs = useTabs()
const [activeTab, setActiveTab] = useState(tabs[0].key)
const [activeTab, setActiveTab] = useState(noBlocks ? TabsEnum.Tools : TabsEnum.Blocks)
return (
<div onClick={e => e.stopPropagation()}>
<div className='flex items-center px-3 border-b-[0.5px] border-b-black/5'>
{
tabs.map(tab => (
<div
key={tab.key}
className={`
relative mr-4 h-[34px] leading-[34px] text-[13px] font-medium cursor-pointer
${activeTab === tab.key
? 'text-gray-700 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-primary-600'
: 'text-gray-500'}
`}
onClick={() => setActiveTab(tab.key)}
>
{tab.name}
</div>
))
}
</div>
{
activeTab === TabsEnum.Blocks && (
!noBlocks && (
<div className='flex items-center px-3 border-b-[0.5px] border-b-black/5'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'relative mr-4 h-[34px] leading-[34px] text-[13px] font-medium cursor-pointer',
activeTab === tab.key
? 'text-gray-700 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-primary-600'
: 'text-gray-500',
)}
onClick={() => setActiveTab(tab.key)}
>
{tab.name}
</div>
))
}
</div>
)
}
{
activeTab === TabsEnum.Blocks && !noBlocks && (
<Blocks
searchText={searchText}
onSelect={onSelect}
@ -53,17 +60,8 @@ const Tabs: FC<TabsProps> = ({
)
}
{
activeTab === TabsEnum.BuiltInTool && (
<Tools
onSelect={onSelect}
searchText={searchText}
/>
)
}
{
activeTab === TabsEnum.CustomTool && (
<Tools
isCustom
activeTab === TabsEnum.Tools && (
<AllTools
searchText={searchText}
onSelect={onSelect}
/>

View File

@ -1,41 +1,28 @@
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import type { ToolWithProvider } from '../types'
import { useStore } from '../store'
import type { ToolDefaultValue } from './types'
import Tooltip from '@/app/components/base/tooltip'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import { useGetLanguage } from '@/context/i18n'
type ToolsProps = {
isCustom?: boolean
showWorkflowEmpty: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
searchText: string
tools: ToolWithProvider[]
}
const Blocks = ({
isCustom,
searchText,
showWorkflowEmpty,
onSelect,
tools,
}: ToolsProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const tools = useMemo(() => {
const currentTools = isCustom ? customTools : buildInTools
return currentTools.filter((toolWithProvider) => {
return toolWithProvider.tools.some((tool) => {
return tool.label[language].toLowerCase().includes(searchText.toLowerCase())
})
})
}, [isCustom, customTools, buildInTools, searchText, language])
const renderGroup = useCallback((toolWithProvider: ToolWithProvider) => {
const list = toolWithProvider.tools
@ -97,10 +84,15 @@ const Blocks = ({
return (
<div className='p-1 max-w-[320px] max-h-[464px] overflow-y-auto'>
{
!tools.length && (
!tools.length && !showWorkflowEmpty && (
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
)
}
{!tools.length && showWorkflowEmpty && (
<div className='py-10'>
<Empty/>
</div>
)}
{
!!tools.length && tools.map(renderGroup)
}

View File

@ -1,7 +1,13 @@
export enum TabsEnum {
Blocks = 'blocks',
BuiltInTool = 'built-in-tool',
CustomTool = 'custom-tool',
Tools = 'tools',
}
export enum ToolTypeEnum {
All = 'all',
BuiltIn = 'built-in',
Custom = 'custom',
Workflow = 'workflow',
}
export enum BlockClassificationEnum {

View File

@ -9,9 +9,11 @@ import IfElseDefault from './nodes/if-else/default'
import CodeDefault from './nodes/code/default'
import TemplateTransformDefault from './nodes/template-transform/default'
import HttpRequestDefault from './nodes/http/default'
import ParameterExtractorDefault from './nodes/parameter-extractor/default'
import ToolDefault from './nodes/tool/default'
import VariableAssignerDefault from './nodes/variable-assigner/default'
import EndNodeDefault from './nodes/end/default'
import IterationDefault from './nodes/iteration/default'
type NodesExtraData = {
author: string
@ -77,6 +79,15 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
getAvailableNextNodes: IfElseDefault.getAvailableNextNodes,
checkValid: IfElseDefault.checkValid,
},
[BlockEnum.Iteration]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: IterationDefault.getAvailablePrevNodes,
getAvailableNextNodes: IterationDefault.getAvailableNextNodes,
checkValid: IterationDefault.checkValid,
},
[BlockEnum.Code]: {
author: 'Dify',
about: '',
@ -122,6 +133,24 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
checkValid: VariableAssignerDefault.checkValid,
},
[BlockEnum.VariableAggregator]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: VariableAssignerDefault.getAvailablePrevNodes,
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
checkValid: VariableAssignerDefault.checkValid,
},
[BlockEnum.ParameterExtractor]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: ParameterExtractorDefault.getAvailablePrevNodes,
getAvailableNextNodes: ParameterExtractorDefault.getAvailableNextNodes,
checkValid: ParameterExtractorDefault.checkValid,
},
[BlockEnum.Tool]: {
author: 'Dify',
about: '',
@ -177,6 +206,12 @@ export const NODES_INITIAL_DATA = {
desc: '',
...IfElseDefault.defaultValue,
},
[BlockEnum.Iteration]: {
type: BlockEnum.Iteration,
title: '',
desc: '',
...IterationDefault.defaultValue,
},
[BlockEnum.Code]: {
type: BlockEnum.Code,
title: '',
@ -210,6 +245,13 @@ export const NODES_INITIAL_DATA = {
variables: [],
...HttpRequestDefault.defaultValue,
},
[BlockEnum.ParameterExtractor]: {
type: BlockEnum.ParameterExtractor,
title: '',
desc: '',
variables: [],
...ParameterExtractorDefault.defaultValue,
},
[BlockEnum.VariableAssigner]: {
type: BlockEnum.VariableAssigner,
title: '',
@ -218,6 +260,14 @@ export const NODES_INITIAL_DATA = {
output_type: '',
...VariableAssignerDefault.defaultValue,
},
[BlockEnum.VariableAggregator]: {
type: BlockEnum.VariableAggregator,
title: '',
desc: '',
variables: [],
output_type: '',
...VariableAssignerDefault.defaultValue,
},
[BlockEnum.Tool]: {
type: BlockEnum.Tool,
title: '',
@ -236,6 +286,14 @@ export const AUTO_LAYOUT_OFFSET = {
x: -42,
y: 243,
}
export const ITERATION_NODE_Z_INDEX = 1
export const ITERATION_CHILDREN_Z_INDEX = 1002
export const ITERATION_PADDING = {
top: 85,
right: 16,
bottom: 20,
left: 16,
}
export const RETRIEVAL_OUTPUT_STRUCT = `{
"content": "",
@ -259,7 +317,8 @@ export const RETRIEVAL_OUTPUT_STRUCT = `{
export const SUPPORT_OUTPUT_VARS_NODE = [
BlockEnum.Start, BlockEnum.LLM, BlockEnum.KnowledgeRetrieval, BlockEnum.Code, BlockEnum.TemplateTransform,
BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.QuestionClassifier,
BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier,
BlockEnum.ParameterExtractor, BlockEnum.Iteration,
]
export const LLM_OUTPUT_STRUCT: Var[] = [
@ -320,4 +379,15 @@ export const TOOL_OUTPUT_STRUCT: Var[] = [
},
]
export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [
{
variable: '__is_success',
type: VarType.number,
},
{
variable: '__reason',
type: VarType.string,
},
]
export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE'

View File

@ -3,6 +3,7 @@ import {
useCallback,
useState,
} from 'react'
import cn from 'classnames'
import { intersection } from 'lodash-es'
import type { EdgeProps } from 'reactflow'
import {
@ -12,7 +13,7 @@ import {
getBezierPath,
} from 'reactflow'
import {
useNodesExtraData,
useAvailableBlocks,
useNodesInteractions,
} from './hooks'
import BlockSelector from './block-selector'
@ -20,6 +21,7 @@ import type {
Edge,
OnSelectBlock,
} from './types'
import { ITERATION_CHILDREN_Z_INDEX } from './constants'
const CustomEdge = ({
id,
@ -49,9 +51,9 @@ const CustomEdge = ({
})
const [open, setOpen] = useState(false)
const { handleNodeAdd } = useNodesInteractions()
const nodesExtraData = useNodesExtraData()
const availablePrevNodes = nodesExtraData[(data as Edge['data'])!.targetType]?.availablePrevNodes || []
const availableNextNodes = nodesExtraData[(data as Edge['data'])!.sourceType]?.availableNextNodes || []
const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration)
const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration)
const handleOpenChange = useCallback((v: boolean) => {
setOpen(v)
}, [])
@ -83,11 +85,12 @@ const CustomEdge = ({
/>
<EdgeLabelRenderer>
<div
className={`
nopan nodrag hover:scale-125
${data?._hovering ? 'block' : 'hidden'}
${open && '!block'}
`}
className={cn(
'nopan nodrag hover:scale-125',
data?._hovering ? 'block' : 'hidden',
open && '!block',
data.isInIteration && `z-[${ITERATION_CHILDREN_Z_INDEX}]`,
)}
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
@ -99,7 +102,7 @@ const CustomEdge = ({
onOpenChange={handleOpenChange}
asChild
onSelect={handleInsert}
availableBlocksTypes={intersection(availablePrevNodes, availableNextNodes)}
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks)}
triggerClassName={() => 'hover:scale-150 transition-all'}
/>
</div>

View File

@ -2,13 +2,20 @@ import type { FC } from 'react'
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
useStore,
useWorkflowStore,
} from '../store'
import {
BlockEnum,
InputVarType,
} from '../types'
import type { StartNodeType } from '../nodes/start/types'
import {
useChecklistBeforePublish,
useNodesReadOnly,
@ -29,6 +36,7 @@ import Button from '@/app/components/base/button'
import { useStore as useAppStore } from '@/app/components/app/store'
import { publishWorkflow } from '@/service/workflow'
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
import { useFeatures } from '@/app/components/base/features/hooks'
const Header: FC = () => {
const { t } = useTranslation()
@ -42,6 +50,28 @@ const Header: FC = () => {
} = useNodesReadOnly()
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const toolPublished = useStore(s => s.toolPublished)
const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const startVariables = startNode?.data.variables
const fileSettings = useFeatures(s => s.features.file)
const variables = useMemo(() => {
const data = startVariables || []
if (fileSettings?.image?.enabled) {
return [
...data,
{
type: InputVarType.files,
variable: '__image',
required: false,
label: 'files',
},
]
}
return data
}, [fileSettings?.image?.enabled, startVariables])
const {
handleLoadBackupDraft,
handleBackupDraft,
@ -108,6 +138,10 @@ const Header: FC = () => {
workflowStore.setState({ historyWorkflowData: undefined })
}, [workflowStore, handleLoadBackupDraft])
const handleToolConfigureUpdate = useCallback(() => {
workflowStore.setState({ toolPublished: true })
}, [workflowStore])
return (
<div
className='absolute top-0 left-0 z-10 flex items-center justify-between w-full px-3 h-14'
@ -152,6 +186,9 @@ const Header: FC = () => {
publishedAt,
draftUpdatedAt,
disabled: Boolean(getNodesReadOnly()),
toolPublished,
inputs: variables,
onRefreshData: handleToolConfigureUpdate,
onPublish,
onRestore: onStartRestoring,
onToggle: onPublisherToggle,

View File

@ -12,3 +12,4 @@ export * from './use-workflow-interactions'
export * from './use-selection-interactions'
export * from './use-panel-interactions'
export * from './use-workflow-start-run'
export * from './use-nodes-layout'

View File

@ -29,6 +29,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const isChatMode = useIsChatMode()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const needWarningNodes = useMemo(() => {
const list = []
@ -41,14 +42,16 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
if (node.data.type === BlockEnum.Tool) {
const { provider_type } = node.data
const isBuiltIn = provider_type === CollectionType.builtIn
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, language)
if (isBuiltIn)
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, workflowTools, language)
if (provider_type === CollectionType.builtIn)
toolIcon = buildInTools.find(tool => tool.id === node.data.provider_id)?.icon
if (!isBuiltIn)
if (provider_type === CollectionType.custom)
toolIcon = customTools.find(tool => tool.id === node.data.provider_id)?.icon
if (provider_type === CollectionType.workflow)
toolIcon = workflowTools.find(tool => tool.id === node.data.provider_id)?.icon
}
const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid)
@ -83,7 +86,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
}
return list
}, [t, nodes, edges, nodesExtraData, buildInTools, customTools, language, isChatMode])
}, [t, nodes, edges, nodesExtraData, buildInTools, customTools, workflowTools, language, isChatMode])
return needWarningNodes
}
@ -93,6 +96,7 @@ export const useChecklistBeforePublish = () => {
const language = useGetLanguage()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const { notify } = useToastContext()
const isChatMode = useIsChatMode()
const store = useStoreApi()
@ -118,7 +122,7 @@ export const useChecklistBeforePublish = () => {
const node = nodes[i]
let moreDataForCheckValid
if (node.data.type === BlockEnum.Tool)
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, language)
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools, customTools, workflowTools, language)
const { errorMessage } = nodesExtraData[node.data.type as BlockEnum].checkValid(node.data, t, moreDataForCheckValid)
@ -144,7 +148,7 @@ export const useChecklistBeforePublish = () => {
}
return true
}, [nodesExtraData, notify, t, store, isChatMode, buildInTools, customTools, language])
}, [nodesExtraData, notify, t, store, isChatMode, buildInTools, customTools, workflowTools, language])
return {
handleCheckBeforePublish,

View File

@ -5,14 +5,11 @@ import type {
OnEdgesChange,
} from 'reactflow'
import {
getConnectedEdges,
useStoreApi,
} from 'reactflow'
import type {
Edge,
Node,
} from '../types'
import { BlockEnum } from '../types'
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../utils'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { useNodesReadOnly } from './use-workflow'
@ -146,61 +143,6 @@ export const useEdgesInteractions = () => {
setEdges(newEdges)
}, [store, getNodesReadOnly])
const handleVariableAssignerEdgesChange = useCallback((nodeId: string, variables: any) => {
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
const nodes = getNodes()
const newEdgesTargetHandleIds = variables.map((item: any) => item[0])
const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges).filter(edge => edge.target === nodeId)
const needDeleteEdges = connectedEdges.filter(edge => !newEdgesTargetHandleIds.includes(edge.targetHandle))
const needAddEdgesTargetHandleIds = newEdgesTargetHandleIds.filter((targetHandle: string) => !connectedEdges.some(edge => edge.targetHandle === targetHandle))
const needAddEdges = needAddEdgesTargetHandleIds.map((targetHandle: string) => {
return {
id: `${targetHandle}-${nodeId}`,
type: 'custom',
source: targetHandle,
sourceHandle: 'source',
target: nodeId,
targetHandle,
data: {
sourceType: nodes.find(node => node.id === targetHandle)?.data.type,
targetType: BlockEnum.VariableAssigner,
},
}
})
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
[
...needDeleteEdges.map(edge => ({ type: 'remove', edge })),
...needAddEdges.map((edge: Edge) => ({ type: 'add', edge })),
],
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) => {
const filtered = draft.filter(edge => !needDeleteEdges.map(needDeleteEdge => needDeleteEdge.id).includes(edge.id))
filtered.push(...needAddEdges)
return filtered
})
setEdges(newEdges)
}, [store])
const handleEdgeCancelRunningStatus = useCallback(() => {
const {
edges,
@ -221,7 +163,6 @@ export const useEdgesInteractions = () => {
handleEdgeDeleteByDeleteBranch,
handleEdgeDelete,
handleEdgesChange,
handleVariableAssignerEdgesChange,
handleEdgeCancelRunningStatus,
}
}

View File

@ -0,0 +1,114 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import type { Node } from '../types'
import { useWorkflowStore } from '../store'
export const useHelpline = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const handleSetHelpline = useCallback((node: Node) => {
const { getNodes } = store.getState()
const nodes = getNodes()
const {
setHelpLineHorizontal,
setHelpLineVertical,
} = workflowStore.getState()
if (node.data.isInIteration) {
return {
showHorizontalHelpLineNodes: [],
showVerticalHelpLineNodes: [],
}
}
const showHorizontalHelpLineNodes = nodes.filter((n) => {
if (n.id === node.id)
return false
if (n.data.isInIteration)
return false
const nY = Math.ceil(n.position.y)
const nodeY = Math.ceil(node.position.y)
if (nY - nodeY < 5 && nY - nodeY > -5)
return true
return false
}).sort((a, b) => a.position.x - b.position.x)
const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
if (showHorizontalHelpLineNodesLength > 0) {
const first = showHorizontalHelpLineNodes[0]
const last = showHorizontalHelpLineNodes[showHorizontalHelpLineNodesLength - 1]
const helpLine = {
top: first.position.y,
left: first.position.x,
width: last.position.x + last.width! - first.position.x,
}
if (node.position.x < first.position.x) {
helpLine.left = node.position.x
helpLine.width = first.position.x + first.width! - node.position.x
}
if (node.position.x > last.position.x)
helpLine.width = node.position.x + node.width! - first.position.x
setHelpLineHorizontal(helpLine)
}
else {
setHelpLineHorizontal()
}
const showVerticalHelpLineNodes = nodes.filter((n) => {
if (n.id === node.id)
return false
if (n.data.isInIteration)
return false
const nX = Math.ceil(n.position.x)
const nodeX = Math.ceil(node.position.x)
if (nX - nodeX < 5 && nX - nodeX > -5)
return true
return false
}).sort((a, b) => a.position.x - b.position.x)
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
if (showVerticalHelpLineNodesLength > 0) {
const first = showVerticalHelpLineNodes[0]
const last = showVerticalHelpLineNodes[showVerticalHelpLineNodesLength - 1]
const helpLine = {
top: first.position.y,
left: first.position.x,
height: last.position.y + last.height! - first.position.y,
}
if (node.position.y < first.position.y) {
helpLine.top = node.position.y
helpLine.height = first.position.y + first.height! - node.position.y
}
if (node.position.y > last.position.y)
helpLine.height = node.position.y + node.height! - first.position.y
setHelpLineVertical(helpLine)
}
else {
setHelpLineVertical()
}
return {
showHorizontalHelpLineNodes,
showVerticalHelpLineNodes,
}
}, [store, workflowStore])
return {
handleSetHelpline,
}
}

View File

@ -22,7 +22,7 @@ export const useNodeDataUpdate = () => {
const newNodes = produce(getNodes(), (draft) => {
const currentNode = draft.find(node => node.id === id)!
currentNode.data = { ...currentNode.data, ...data }
currentNode.data = { ...currentNode?.data, ...data }
})
setNodes(newNodes)
}, [store])

View File

@ -1,7 +1,7 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import type { BlockEnum } from '../types'
import { BlockEnum } from '../types'
import {
NODES_EXTRA_DATA,
NODES_INITIAL_DATA,
@ -30,3 +30,33 @@ export const useNodesExtraData = () => {
})
}), [t, isChatMode])
}
export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean) => {
const nodesExtraData = useNodesExtraData()
const availablePrevBlocks = useMemo(() => {
if (!nodeType)
return []
return nodesExtraData[nodeType].availablePrevNodes || []
}, [nodeType, nodesExtraData])
const availableNextBlocks = useMemo(() => {
if (!nodeType)
return []
return nodesExtraData[nodeType].availableNextNodes || []
}, [nodeType, nodesExtraData])
return useMemo(() => {
return {
availablePrevBlocks: availablePrevBlocks.filter((nType) => {
if (isInIteration && (nType === BlockEnum.Iteration || nType === BlockEnum.End))
return false
return true
}),
availableNextBlocks: availableNextBlocks.filter((nType) => {
if (isInIteration && (nType === BlockEnum.Iteration || nType === BlockEnum.End))
return false
return true
}),
}
}, [isInIteration, availablePrevBlocks, availableNextBlocks])
}

View File

@ -3,11 +3,12 @@ import { useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import type {
HandleType,
NodeDragHandler,
NodeMouseHandler,
OnConnect,
OnConnectEnd,
OnConnectStart,
ResizeParamsWithDirection,
} from 'reactflow'
import {
getConnectedEdges,
@ -24,8 +25,11 @@ import type {
import { BlockEnum } from '../types'
import { useWorkflowStore } from '../store'
import {
ITERATION_CHILDREN_Z_INDEX,
ITERATION_PADDING,
NODES_INITIAL_DATA,
NODE_WIDTH_X_OFFSET,
X_OFFSET,
Y_OFFSET,
} from '../constants'
import {
@ -33,8 +37,11 @@ import {
getNodesConnectedSourceOrTargetHandleIdsMap,
getTopLeftNodePosition,
} from '../utils'
import { useNodesExtraData } from './use-nodes-data'
import type { IterationNodeType } from '../nodes/iteration/types'
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { useHelpline } from './use-helpline'
import {
useNodesReadOnly,
useWorkflow,
@ -45,15 +52,17 @@ export const useNodesInteractions = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const nodesExtraData = useNodesExtraData()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const {
getAfterNodesInSameBranch,
getTreeLeafNodes,
} = useWorkflow()
const { getNodesReadOnly } = useNodesReadOnly()
const { handleSetHelpline } = useHelpline()
const {
handleNodeIterationChildDrag,
handleNodeIterationChildrenCopy,
} = useNodeIterationInteractions()
const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number })
const connectingNodeRef = useRef<{ nodeId: string; handleType: HandleType } | null>(null)
const handleNodeDragStart = useCallback<NodeDragHandler>((_, node) => {
workflowStore.setState({ nodeAnimation: false })
@ -61,6 +70,9 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly())
return
if (node.data.isIterationStart)
return
dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
}, [workflowStore, getNodesReadOnly])
@ -68,104 +80,46 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly())
return
if (node.data.isIterationStart)
return
const {
getNodes,
setNodes,
} = store.getState()
const {
setHelpLineHorizontal,
setHelpLineVertical,
} = workflowStore.getState()
e.stopPropagation()
const nodes = getNodes()
const showHorizontalHelpLineNodes = nodes.filter((n) => {
if (n.id === node.id)
return false
const { restrictPosition } = handleNodeIterationChildDrag(node)
const nY = Math.ceil(n.position.y)
const nodeY = Math.ceil(node.position.y)
if (nY - nodeY < 5 && nY - nodeY > -5)
return true
return false
}).sort((a, b) => a.position.x - b.position.x)
const {
showHorizontalHelpLineNodes,
showVerticalHelpLineNodes,
} = handleSetHelpline(node)
const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
if (showHorizontalHelpLineNodesLength > 0) {
const first = showHorizontalHelpLineNodes[0]
const last = showHorizontalHelpLineNodes[showHorizontalHelpLineNodesLength - 1]
const helpLine = {
top: first.position.y,
left: first.position.x,
width: last.position.x + last.width! - first.position.x,
}
if (node.position.x < first.position.x) {
helpLine.left = node.position.x
helpLine.width = first.position.x + first.width! - node.position.x
}
if (node.position.x > last.position.x)
helpLine.width = node.position.x + node.width! - first.position.x
setHelpLineHorizontal(helpLine)
}
else {
setHelpLineHorizontal()
}
const showVerticalHelpLineNodes = nodes.filter((n) => {
if (n.id === node.id)
return false
const nX = Math.ceil(n.position.x)
const nodeX = Math.ceil(node.position.x)
if (nX - nodeX < 5 && nX - nodeX > -5)
return true
return false
}).sort((a, b) => a.position.x - b.position.x)
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
if (showVerticalHelpLineNodesLength > 0) {
const first = showVerticalHelpLineNodes[0]
const last = showVerticalHelpLineNodes[showVerticalHelpLineNodesLength - 1]
const helpLine = {
top: first.position.y,
left: first.position.x,
height: last.position.y + last.height! - first.position.y,
}
if (node.position.y < first.position.y) {
helpLine.top = node.position.y
helpLine.height = first.position.y + first.height! - node.position.y
}
if (node.position.y > last.position.y)
helpLine.height = node.position.y + node.height! - first.position.y
setHelpLineVertical(helpLine)
}
else {
setHelpLineVertical()
}
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(n => n.id === node.id)!
currentNode.position = {
x: showVerticalHelpLineNodesLength > 0 ? showVerticalHelpLineNodes[0].position.x : node.position.x,
y: showHorizontalHelpLineNodesLength > 0 ? showHorizontalHelpLineNodes[0].position.y : node.position.y,
}
if (showVerticalHelpLineNodesLength > 0)
currentNode.position.x = showVerticalHelpLineNodes[0].position.x
else if (restrictPosition.x !== undefined)
currentNode.position.x = restrictPosition.x
else
currentNode.position.x = node.position.x
if (showHorizontalHelpLineNodesLength > 0)
currentNode.position.y = showHorizontalHelpLineNodes[0].position.y
else if (restrictPosition.y !== undefined)
currentNode.position.y = restrictPosition.y
else
currentNode.position.y = node.position.y
})
setNodes(newNodes)
}, [store, workflowStore, getNodesReadOnly])
}, [store, getNodesReadOnly, handleSetHelpline, handleNodeIterationChildDrag])
const handleNodeDragStop = useCallback<NodeDragHandler>((_, node) => {
const {
@ -195,20 +149,35 @@ export const useNodesInteractions = () => {
setEdges,
} = store.getState()
const nodes = getNodes()
const {
connectingNodePayload,
setEnteringNodePayload,
} = workflowStore.getState()
if (connectingNodeRef.current && connectingNodeRef.current.nodeId !== node.id) {
const connectingNode: Node = nodes.find(n => n.id === connectingNodeRef.current!.nodeId)!
const handleType = connectingNodeRef.current.handleType
const currentNodeIndex = nodes.findIndex(n => n.id === node.id)
const availablePrevNodes = nodesExtraData[connectingNode.data.type].availablePrevNodes
const availableNextNodes = nodesExtraData[connectingNode.data.type].availableNextNodes
const availableNodes = handleType === 'source' ? availableNextNodes : [...availablePrevNodes, BlockEnum.Start]
if (connectingNodePayload) {
if (connectingNodePayload.nodeId === node.id)
return
const connectingNode: Node = nodes.find(n => n.id === connectingNodePayload.nodeId)!
const sameLevel = connectingNode.parentId === node.parentId
const newNodes = produce(nodes, (draft) => {
if (!availableNodes.includes(draft[currentNodeIndex].data.type))
draft[currentNodeIndex].data._isInvalidConnection = true
})
setNodes(newNodes)
if (sameLevel) {
setEnteringNodePayload({
nodeId: node.id,
})
const fromType = connectingNodePayload.handleType
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
if (n.id === node.id && fromType === 'source' && (node.data.type === BlockEnum.VariableAssigner || node.data.type === BlockEnum.VariableAggregator)) {
if (!node.data.advanced_settings?.group_enabled)
n.data._isEntering = true
}
if (n.id === node.id && fromType === 'target' && (connectingNode.data.type === BlockEnum.VariableAssigner || connectingNode.data.type === BlockEnum.VariableAggregator) && node.data.type !== BlockEnum.IfElse && node.data.type !== BlockEnum.QuestionClassifier)
n.data._isEntering = true
})
})
setNodes(newNodes)
}
}
const newEdges = produce(edges, (draft) => {
const connectedEdges = getConnectedEdges([node], edges)
@ -220,12 +189,16 @@ export const useNodesInteractions = () => {
})
})
setEdges(newEdges)
}, [store, nodesExtraData, getNodesReadOnly])
}, [store, workflowStore, getNodesReadOnly])
const handleNodeLeave = useCallback<NodeMouseHandler>(() => {
if (getNodesReadOnly())
return
const {
setEnteringNodePayload,
} = workflowStore.getState()
setEnteringNodePayload(undefined)
const {
getNodes,
setNodes,
@ -234,7 +207,7 @@ export const useNodesInteractions = () => {
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data._isInvalidConnection = false
node.data._isEntering = false
})
})
setNodes(newNodes)
@ -244,7 +217,7 @@ export const useNodesInteractions = () => {
})
})
setEdges(newEdges)
}, [store, getNodesReadOnly])
}, [store, workflowStore, getNodesReadOnly])
const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => {
const {
@ -315,16 +288,18 @@ export const useNodesInteractions = () => {
} = store.getState()
const nodes = getNodes()
const targetNode = nodes.find(node => node.id === target!)
if (targetNode && targetNode?.data.type === BlockEnum.VariableAssigner) {
const treeNodes = getTreeLeafNodes(target!)
const sourceNode = nodes.find(node => node.id === source!)
if (targetNode?.parentId !== sourceNode?.parentId)
return
if (targetNode?.data.isIterationStart)
return
if (!treeNodes.find(treeNode => treeNode.id === source))
return
}
const needDeleteEdges = edges.filter((edge) => {
if (
(edge.source === source && edge.sourceHandle === sourceHandle)
|| (edge.target === target && edge.targetHandle === targetHandle)
|| (edge.target === target && edge.targetHandle === targetHandle && targetNode?.data.type !== BlockEnum.VariableAssigner && targetNode?.data.type !== BlockEnum.VariableAggregator)
)
return true
@ -332,7 +307,7 @@ export const useNodesInteractions = () => {
})
const needDeleteEdgesIds = needDeleteEdges.map(edge => edge.id)
const newEdge = {
id: `${source}-${target}`,
id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
type: 'custom',
source: source!,
target: target!,
@ -341,7 +316,10 @@ export const useNodesInteractions = () => {
data: {
sourceType: nodes.find(node => node.id === source)!.data.type,
targetType: nodes.find(node => node.id === target)!.data.type,
isInIteration: !!targetNode?.parentId,
iteration_id: targetNode?.parentId,
},
zIndex: targetNode?.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
}
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
[
@ -370,20 +348,126 @@ export const useNodesInteractions = () => {
})
setEdges(newEdges)
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, getTreeLeafNodes])
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType, handleId }) => {
if (getNodesReadOnly())
return
const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType }) => {
if (nodeId && handleType) {
connectingNodeRef.current = {
nodeId,
handleType,
const { setConnectingNodePayload } = workflowStore.getState()
const { getNodes } = store.getState()
const node = getNodes().find(n => n.id === nodeId)!
if (!node.data.isIterationStart) {
setConnectingNodePayload({
nodeId,
nodeType: node.data.type,
handleType,
handleId,
})
}
}
}, [])
}, [store, workflowStore, getNodesReadOnly])
const handleNodeConnectEnd = useCallback(() => {
connectingNodeRef.current = null
}, [])
const handleNodeConnectEnd = useCallback<OnConnectEnd>((e: any) => {
if (getNodesReadOnly())
return
const {
connectingNodePayload,
setConnectingNodePayload,
enteringNodePayload,
setEnteringNodePayload,
} = workflowStore.getState()
if (connectingNodePayload && enteringNodePayload) {
const {
setShowAssignVariablePopup,
hoveringAssignVariableGroupId,
} = workflowStore.getState()
const { screenToFlowPosition } = reactflow
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const fromHandleType = connectingNodePayload.handleType
const fromHandleId = connectingNodePayload.handleId
const fromNode = nodes.find(n => n.id === connectingNodePayload.nodeId)!
const fromNodeParent = nodes.find(n => n.id === fromNode.parentId)
const toNode = nodes.find(n => n.id === enteringNodePayload.nodeId)!
const toParentNode = nodes.find(n => n.id === toNode.parentId)
if (fromNode.parentId !== toNode.parentId)
return
const { x, y } = screenToFlowPosition({ x: e.x, y: e.y })
if (fromHandleType === 'source' && (toNode.data.type === BlockEnum.VariableAssigner || toNode.data.type === BlockEnum.VariableAggregator)) {
const groupEnabled = toNode.data.advanced_settings?.group_enabled
if (
(groupEnabled && hoveringAssignVariableGroupId)
|| !groupEnabled
) {
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
if (node.id === toNode.id) {
node.data._showAddVariablePopup = true
node.data._holdAddVariablePopup = true
}
})
})
setNodes(newNodes)
setShowAssignVariablePopup({
nodeId: fromNode.id,
nodeData: fromNode.data,
variableAssignerNodeId: toNode.id,
variableAssignerNodeData: toNode.data,
variableAssignerNodeHandleId: hoveringAssignVariableGroupId || 'target',
parentNode: toParentNode,
x: x - toNode.positionAbsolute!.x,
y: y - toNode.positionAbsolute!.y,
})
handleNodeConnect({
source: fromNode.id,
sourceHandle: fromHandleId,
target: toNode.id,
targetHandle: hoveringAssignVariableGroupId || 'target',
})
}
}
if (fromHandleType === 'target' && (fromNode.data.type === BlockEnum.VariableAssigner || fromNode.data.type === BlockEnum.VariableAggregator) && toNode.data.type !== BlockEnum.IfElse && toNode.data.type !== BlockEnum.QuestionClassifier) {
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
if (node.id === toNode.id) {
node.data._showAddVariablePopup = true
node.data._holdAddVariablePopup = true
}
})
})
setNodes(newNodes)
setShowAssignVariablePopup({
nodeId: toNode.id,
nodeData: toNode.data,
variableAssignerNodeId: fromNode.id,
variableAssignerNodeData: fromNode.data,
variableAssignerNodeHandleId: fromHandleId || 'target',
parentNode: fromNodeParent,
x: x - toNode.positionAbsolute!.x,
y: y - toNode.positionAbsolute!.y,
})
handleNodeConnect({
source: toNode.id,
sourceHandle: 'source',
target: fromNode.id,
targetHandle: fromHandleId,
})
}
}
setConnectingNodePayload(undefined)
setEnteringNodePayload(undefined)
}, [store, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow])
const handleNodeDelete = useCallback((nodeId: string) => {
if (getNodesReadOnly())
@ -398,8 +482,45 @@ export const useNodesInteractions = () => {
const nodes = getNodes()
const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
if (nodes[currentNodeIndex].data.type === BlockEnum.Start)
const currentNode = nodes[currentNodeIndex]
if (!currentNode)
return
if (currentNode.data.type === BlockEnum.Start)
return
if (currentNode.data.type === BlockEnum.Iteration) {
const iterationChildren = nodes.filter(node => node.parentId === currentNode.id)
if (iterationChildren.length) {
if (currentNode.data._isBundled) {
iterationChildren.forEach((child) => {
handleNodeDelete(child.id)
})
return handleNodeDelete(nodeId)
}
else {
const { setShowConfirm, showConfirm } = workflowStore.getState()
if (!showConfirm) {
setShowConfirm({
title: t('workflow.nodes.iteration.deleteTitle'),
desc: t('workflow.nodes.iteration.deleteDesc') || '',
onConfirm: () => {
iterationChildren.forEach((child) => {
handleNodeDelete(child.id)
})
handleNodeDelete(nodeId)
handleSyncWorkflowDraft()
setShowConfirm(undefined)
},
})
return
}
}
}
}
const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges)
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(connectedEdges.map(edge => ({ type: 'remove', edge })), nodes)
const newNodes = produce(nodes, (draft: Node[]) => {
@ -410,6 +531,15 @@ export const useNodesInteractions = () => {
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (node.id === currentNode.parentId) {
node.data._children = node.data._children?.filter(child => child !== nodeId)
if (currentNode.id === (node as Node<IterationNodeType>).data.start_node_id) {
(node as Node<IterationNodeType>).data.start_node_id = '';
(node as Node<IterationNodeType>).data.startNodeType = undefined
}
}
})
draft.splice(currentNodeIndex, 1)
})
@ -419,7 +549,7 @@ export const useNodesInteractions = () => {
})
setEdges(newEdges)
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, getNodesReadOnly])
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, workflowStore, t])
const handleNodeAdd = useCallback<OnNodeAdd>((
{
@ -438,9 +568,6 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly())
return
if (nodeType === BlockEnum.VariableAssigner)
targetHandle = 'varNotSet'
const {
getNodes,
setNodes,
@ -455,6 +582,8 @@ export const useNodesInteractions = () => {
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
...(toolDefaultValue || {}),
selected: true,
_showAddVariablePopup: (nodeType === BlockEnum.VariableAssigner || nodeType === BlockEnum.VariableAggregator) && !!prevNodeId,
_holdAddVariablePopup: false,
},
position: {
x: 0,
@ -470,12 +599,19 @@ export const useNodesInteractions = () => {
newNode.data._connectedTargetHandleIds = [targetHandle]
newNode.data._connectedSourceHandleIds = []
newNode.position = {
x: lastOutgoer ? lastOutgoer.position.x : prevNode.position.x + NODE_WIDTH_X_OFFSET,
x: lastOutgoer ? lastOutgoer.position.x : prevNode.position.x + prevNode.width! + X_OFFSET,
y: lastOutgoer ? lastOutgoer.position.y + lastOutgoer.height! + Y_OFFSET : prevNode.position.y,
}
newNode.parentId = prevNode.parentId
newNode.extent = prevNode.extent
if (prevNode.parentId) {
newNode.data.isInIteration = true
newNode.data.iteration_id = prevNode.parentId
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
}
const newEdge = {
id: `${prevNodeId}-${newNode.id}`,
const newEdge: Edge = {
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
type: 'custom',
source: prevNodeId,
sourceHandle: prevNodeSourceHandle,
@ -484,8 +620,11 @@ export const useNodesInteractions = () => {
data: {
sourceType: prevNode.data.type,
targetType: newNode.data.type,
isInIteration: !!prevNode.parentId,
iteration_id: prevNode.parentId,
_connectedNodeIsSelected: true,
},
zIndex: prevNode.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
}
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
[
@ -503,10 +642,27 @@ export const useNodesInteractions = () => {
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
node.data._children?.push(newNode.id)
})
draft.push(newNode)
})
setNodes(newNodes)
if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
const { setShowAssignVariablePopup } = workflowStore.getState()
setShowAssignVariablePopup({
nodeId: prevNode.id,
nodeData: prevNode.data,
variableAssignerNodeId: newNode.id,
variableAssignerNodeData: (newNode.data as VariableAssignerNodeType),
variableAssignerNodeHandleId: targetHandle,
parentNode: nodes.find(node => node.id === newNode.parentId),
x: -25,
y: 44,
})
}
const newEdges = produce(edges, (draft) => {
draft.forEach((item) => {
item.data = {
@ -528,12 +684,21 @@ export const useNodesInteractions = () => {
x: nextNode.position.x,
y: nextNode.position.y,
}
newNode.parentId = nextNode.parentId
newNode.extent = nextNode.extent
if (nextNode.parentId) {
newNode.data.isInIteration = true
newNode.data.iteration_id = nextNode.parentId
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
}
if (nextNode.data.isIterationStart)
newNode.data.isIterationStart = true
let newEdge
if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier)) {
newEdge = {
id: `${newNode.id}-${nextNodeId}`,
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
type: 'custom',
source: newNode.id,
sourceHandle,
@ -542,8 +707,11 @@ export const useNodesInteractions = () => {
data: {
sourceType: newNode.data.type,
targetType: nextNode.data.type,
isInIteration: !!nextNode.parentId,
iteration_id: nextNode.parentId,
_connectedNodeIsSelected: true,
},
zIndex: nextNode.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
}
}
@ -572,6 +740,17 @@ export const useNodesInteractions = () => {
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (node.data.type === BlockEnum.Iteration && nextNode.parentId === node.id)
node.data._children?.push(newNode.id)
if (node.data.type === BlockEnum.Iteration && node.data.start_node_id === nextNodeId) {
node.data.start_node_id = newNode.id
node.data.startNodeType = newNode.data.type
}
if (node.id === nextNodeId && node.data.isIterationStart)
node.data.isIterationStart = false
})
draft.push(newNode)
})
@ -599,10 +778,17 @@ export const useNodesInteractions = () => {
x: nextNode.position.x,
y: nextNode.position.y,
}
newNode.parentId = prevNode.parentId
newNode.extent = prevNode.extent
if (prevNode.parentId) {
newNode.data.isInIteration = true
newNode.data.iteration_id = prevNode.parentId
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
}
const currentEdgeIndex = edges.findIndex(edge => edge.source === prevNodeId && edge.target === nextNodeId)
const newPrevEdge = {
id: `${prevNodeId}-${newNode.id}`,
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
type: 'custom',
source: prevNodeId,
sourceHandle: prevNodeSourceHandle,
@ -611,13 +797,16 @@ export const useNodesInteractions = () => {
data: {
sourceType: prevNode.data.type,
targetType: newNode.data.type,
isInIteration: !!prevNode.parentId,
iteration_id: prevNode.parentId,
_connectedNodeIsSelected: true,
},
zIndex: prevNode.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
}
let newNextEdge: Edge | null = null
if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier) {
newNextEdge = {
id: `${newNode.id}-${nextNodeId}`,
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
type: 'custom',
source: newNode.id,
sourceHandle,
@ -626,8 +815,11 @@ export const useNodesInteractions = () => {
data: {
sourceType: newNode.data.type,
targetType: nextNode.data.type,
isInIteration: !!nextNode.parentId,
iteration_id: nextNode.parentId,
_connectedNodeIsSelected: true,
},
zIndex: nextNode.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
}
}
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
@ -653,10 +845,27 @@ export const useNodesInteractions = () => {
}
if (afterNodesInSameBranchIds.includes(node.id))
node.position.x += NODE_WIDTH_X_OFFSET
if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
node.data._children?.push(newNode.id)
})
draft.push(newNode)
})
setNodes(newNodes)
if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
const { setShowAssignVariablePopup } = workflowStore.getState()
setShowAssignVariablePopup({
nodeId: prevNode.id,
nodeData: prevNode.data,
variableAssignerNodeId: newNode.id,
variableAssignerNodeData: newNode.data as VariableAssignerNodeType,
variableAssignerNodeHandleId: targetHandle,
parentNode: nodes.find(node => node.id === newNode.parentId),
x: -25,
y: 44,
})
}
const newEdges = produce(edges, (draft) => {
draft.splice(currentEdgeIndex, 1)
draft.forEach((item) => {
@ -673,7 +882,7 @@ export const useNodesInteractions = () => {
setEdges(newEdges)
}
handleSyncWorkflowDraft()
}, [store, handleSyncWorkflowDraft, getAfterNodesInSameBranch, getNodesReadOnly, t])
}, [store, workflowStore, handleSyncWorkflowDraft, getAfterNodesInSameBranch, getNodesReadOnly, t])
const handleNodeChange = useCallback((
currentNodeId: string,
@ -702,11 +911,17 @@ export const useNodesInteractions = () => {
_connectedSourceHandleIds: [],
_connectedTargetHandleIds: [],
selected: currentNode.data.selected,
isInIteration: currentNode.data.isInIteration,
iteration_id: currentNode.data.iteration_id,
isIterationStart: currentNode.data.isIterationStart,
},
position: {
x: currentNode.position.x,
y: currentNode.position.y,
},
parentId: currentNode.parentId,
extent: currentNode.extent,
zIndex: currentNode.zIndex,
})
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
[
@ -724,6 +939,14 @@ export const useNodesInteractions = () => {
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
if (node.id === currentNode.parentId && currentNode.data.isIterationStart) {
node.data._children = [
newCurrentNode.id,
...(node.data._children || []),
].filter(child => child !== currentNodeId)
node.data.start_node_id = newCurrentNode.id
node.data.startNodeType = newCurrentNode.data.type
}
})
const index = draft.findIndex(node => node.id === currentNodeId)
@ -801,7 +1024,7 @@ export const useNodesInteractions = () => {
} = store.getState()
const nodes = getNodes()
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start)
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && !node.data.isInIteration)
if (bundledNodes.length) {
setClipboardElements(bundledNodes)
@ -860,51 +1083,43 @@ export const useNodesInteractions = () => {
x: nodeToPaste.position.x + offsetX,
y: nodeToPaste.position.y + offsetY,
},
extent: nodeToPaste.extent,
zIndex: nodeToPaste.zIndex,
})
newNode.id = newNode.id + index
let newChildren: Node[] = []
if (nodeToPaste.data.type === BlockEnum.Iteration) {
newNode.data._children = [];
(newNode.data as IterationNodeType).start_node_id = ''
newChildren = handleNodeIterationChildrenCopy(nodeToPaste.id, newNode.id)
newChildren.forEach((child) => {
newNode.data._children?.push(child.id)
if (child.data.isIterationStart)
(newNode.data as IterationNodeType).start_node_id = child.id
})
}
nodesToPaste.push(newNode)
if (newChildren.length)
nodesToPaste.push(...newChildren)
})
setNodes([...nodes, ...nodesToPaste])
handleSyncWorkflowDraft()
}
}, [t, getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, reactflow])
}, [t, getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, reactflow, handleNodeIterationChildrenCopy])
const handleNodesDuplicate = useCallback(() => {
if (getNodesReadOnly())
return
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start)
if (selectedNode) {
const nodeType = selectedNode.data.type
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
const newNode = generateNewNode({
data: {
...NODES_INITIAL_DATA[nodeType as BlockEnum],
...selectedNode.data,
selected: false,
_isBundled: false,
_connectedSourceHandleIds: [],
_connectedTargetHandleIds: [],
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
},
position: {
x: selectedNode.position.x + selectedNode.width! + 10,
y: selectedNode.position.y,
},
})
setNodes([...nodes, newNode])
}
}, [store, t, getNodesReadOnly])
handleNodesCopy()
handleNodesPaste()
}, [getNodesReadOnly, handleNodesCopy, handleNodesPaste])
const handleNodesDelete = useCallback(() => {
if (getNodesReadOnly())
@ -928,6 +1143,7 @@ export const useNodesInteractions = () => {
if (bundledNodes.length) {
bundledNodes.forEach(node => handleNodeDelete(node.id))
return
}
@ -941,6 +1157,61 @@ export const useNodesInteractions = () => {
handleNodeDelete(selectedNode.id)
}, [store, workflowStore, getNodesReadOnly, handleNodeDelete])
const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => {
if (getNodesReadOnly())
return
const {
getNodes,
setNodes,
} = store.getState()
const { x, y, width, height } = params
const nodes = getNodes()
const currentNode = nodes.find(n => n.id === nodeId)!
const childrenNodes = nodes.filter(n => currentNode.data._children?.includes(n.id))
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
}
})
if (rightNode! && bottomNode!) {
if (width < rightNode!.position.x + rightNode.width! + ITERATION_PADDING.right)
return
if (height < bottomNode.position.y + bottomNode.height! + ITERATION_PADDING.bottom)
return
}
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
if (n.id === nodeId) {
n.data.width = width
n.data.height = height
n.width = width
n.height = height
n.position.x = x
n.position.y = y
}
})
})
setNodes(newNodes)
handleSyncWorkflowDraft()
}, [store, getNodesReadOnly, handleSyncWorkflowDraft])
return {
handleNodeDragStart,
handleNodeDrag,
@ -962,5 +1233,6 @@ export const useNodesInteractions = () => {
handleNodesPaste,
handleNodesDuplicate,
handleNodesDelete,
handleNodeResize,
}
}

View File

@ -0,0 +1,96 @@
import { useCallback } from 'react'
import ELK from 'elkjs/lib/elk.bundled.js'
import {
useReactFlow,
useStoreApi,
} from 'reactflow'
import { cloneDeep } from 'lodash-es'
import type {
Edge,
Node,
} from '../types'
import { useWorkflowStore } from '../store'
import { AUTO_LAYOUT_OFFSET } from '../constants'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
const layoutOptions = {
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT',
'elk.layered.spacing.nodeNodeBetweenLayers': '60',
'elk.spacing.nodeNode': '40',
'elk.layered.nodePlacement.strategy': 'SIMPLE',
}
const elk = new ELK()
export const getLayoutedNodes = async (nodes: Node[], edges: Edge[]) => {
const graph = {
id: 'root',
layoutOptions,
children: nodes.map((n) => {
return {
...n,
width: n.width ?? 150,
height: n.height ?? 50,
targetPosition: 'left',
sourcePosition: 'right',
}
}),
edges: cloneDeep(edges),
}
const layoutedGraph = await elk.layout(graph as any)
const layoutedNodes = nodes.map((node) => {
const layoutedNode = layoutedGraph.children?.find(
lgNode => lgNode.id === node.id,
)
return {
...node,
position: {
x: (layoutedNode?.x ?? 0) + AUTO_LAYOUT_OFFSET.x,
y: (layoutedNode?.y ?? 0) + AUTO_LAYOUT_OFFSET.y,
},
}
})
return {
layoutedNodes,
}
}
export const useNodesLayout = () => {
const store = useStoreApi()
const reactflow = useReactFlow()
const workflowStore = useWorkflowStore()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const handleNodesLayout = useCallback(async () => {
workflowStore.setState({ nodeAnimation: true })
const {
getNodes,
edges,
setNodes,
} = store.getState()
const { setViewport } = reactflow
const nodes = getNodes()
const {
layoutedNodes,
} = await getLayoutedNodes(nodes, edges)
setNodes(layoutedNodes)
const zoom = 0.7
setViewport({
x: 0,
y: 0,
zoom,
})
setTimeout(() => {
handleSyncWorkflowDraft()
})
}, [store, reactflow, handleSyncWorkflowDraft, workflowStore])
return {
handleNodesLayout,
}
}

View File

@ -85,6 +85,7 @@ export const useWorkflowRun = () => {
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data.selected = false
node.data._runningStatus = undefined
})
})
setNodes(newNodes)
@ -95,6 +96,9 @@ export const useWorkflowRun = () => {
onWorkflowFinished,
onNodeStarted,
onNodeFinished,
onIterationStart,
onIterationNext,
onIterationFinish,
onError,
...restCallback
} = callback || {}
@ -127,6 +131,9 @@ export const useWorkflowRun = () => {
resultText: '',
})
let isInIteration = false
let iterationLength = 0
ssePost(
url,
{
@ -176,7 +183,7 @@ export const useWorkflowRun = () => {
draft.result = {
...draft.result,
...data,
}
} as any
}))
prevNodeId = ''
@ -213,39 +220,53 @@ export const useWorkflowRun = () => {
setEdges,
transform,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
} as any)
}))
if (isInIteration) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iterations = tracing[tracing.length - 1]
const currIteration = iterations.details![iterations.details!.length - 1]
currIteration.push({
...data,
status: NodeRunningStatus.Running,
} as any)
}))
}
else {
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
} as any)
}))
const {
setViewport,
} = reactflow
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex]
const position = currentNode.position
const zoom = transform[2]
const {
setViewport,
} = reactflow
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex]
const position = currentNode.position
const zoom = transform[2]
setViewport({
x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
if (edge)
edge.data = { ...edge.data, _runned: true } as any
})
setEdges(newEdges)
if (!currentNode.parentId) {
setViewport({
x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
}
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
if (edge)
edge.data = { ...edge.data, _runned: true } as any
})
setEdges(newEdges)
}
if (onNodeStarted)
onNodeStarted(params)
},
@ -259,31 +280,166 @@ export const useWorkflowRun = () => {
getNodes,
setNodes,
} = store.getState()
if (isInIteration) {
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const iterations = tracing[tracing.length - 1]
const currIteration = iterations.details![iterations.details!.length - 1]
const nodeInfo = currIteration[currIteration.length - 1]
currIteration[currIteration.length - 1] = {
...nodeInfo,
...data,
status: NodeRunningStatus.Succeeded,
} as any
}))
}
else {
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id)
if (currentIndex > -1 && draft.tracing) {
draft.tracing[currentIndex] = {
...(draft.tracing[currentIndex].extras
? { extras: draft.tracing[currentIndex].extras }
: {}),
...data,
} as any
}
}))
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._runningStatus = data.status as any
})
setNodes(newNodes)
prevNodeId = data.node_id
}
if (onNodeFinished)
onNodeFinished(params)
},
onIterationStart: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const {
getNodes,
setNodes,
edges,
setEdges,
transform,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id)
if (currentIndex > -1 && draft.tracing) {
draft.tracing[currentIndex] = {
...(draft.tracing[currentIndex].extras
? { extras: draft.tracing[currentIndex].extras }
: {}),
...data,
} as any
}
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
details: [],
} as any)
}))
isInIteration = true
iterationLength = data.metadata.iterator_length
const {
setViewport,
} = reactflow
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
const currentNode = nodes[currentNodeIndex]
const position = currentNode.position
const zoom = transform[2]
if (!currentNode.parentId) {
setViewport({
x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
zoom: transform[2],
})
}
const newNodes = produce(nodes, (draft) => {
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
draft[currentNodeIndex].data._iterationLength = data.metadata.iterator_length
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
if (edge)
edge.data = { ...edge.data, _runned: true } as any
})
setEdges(newEdges)
if (onIterationStart)
onIterationStart(params)
},
onIterationNext: (params) => {
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const { data } = params
const {
getNodes,
setNodes,
} = store.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const iteration = draft.tracing![draft.tracing!.length - 1]
if (iteration.details!.length >= iterationLength)
return
iteration.details!.push([])
}))
const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._iterationIndex = data.index > 0 ? data.index : 1
})
setNodes(newNodes)
if (onIterationNext)
onIterationNext(params)
},
onIterationFinish: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
tracing[tracing.length - 1] = {
...tracing[tracing.length - 1],
...data,
status: NodeRunningStatus.Succeeded,
} as any
}))
isInIteration = false
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._runningStatus = data.status as any
currentNode.data._runningStatus = data.status
})
setNodes(newNodes)
prevNodeId = data.node_id
if (onNodeFinished)
onNodeFinished(params)
if (onIterationFinish)
onIterationFinish(params)
},
onTextChunk: (params) => {
const { data: { text } } = params

View File

@ -21,6 +21,7 @@ import {
getLayoutByDagre,
} from '../utils'
import type {
Edge,
Node,
ValueSelector,
} from '../types'
@ -33,7 +34,6 @@ import {
useWorkflowStore,
} from '../store'
import {
AUTO_LAYOUT_OFFSET,
SUPPORT_OUTPUT_VARS_NODE,
} from '../constants'
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
@ -51,8 +51,10 @@ import type { FetchWorkflowDraftResponse } from '@/types/workflow'
import {
fetchAllBuiltInTools,
fetchAllCustomTools,
fetchAllWorkflowTools,
} from '@/service/tools'
import I18n from '@/context/i18n'
import { CollectionType } from '@/app/components/tools/types'
export const useIsChatMode = () => {
const appDetail = useAppStore(s => s.appDetail)
@ -83,13 +85,31 @@ export const useWorkflow = () => {
const { setViewport } = reactflow
const nodes = getNodes()
const layout = getLayoutByDagre(nodes, edges)
const rankMap = {} as Record<string, Node>
nodes.forEach((node) => {
if (!node.parentId) {
const rank = layout.node(node.id).rank!
if (!rankMap[rank]) {
rankMap[rank] = node
}
else {
if (rankMap[rank].position.y > node.position.y)
rankMap[rank] = node
}
}
})
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
const nodeWithPosition = layout.node(node.id)
node.position = {
x: nodeWithPosition.x + AUTO_LAYOUT_OFFSET.x,
y: nodeWithPosition.y + AUTO_LAYOUT_OFFSET.y,
if (!node.parentId) {
const nodeWithPosition = layout.node(node.id)
node.position = {
x: nodeWithPosition.x - node.width! / 2,
y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
}
}
})
})
@ -111,7 +131,11 @@ export const useWorkflow = () => {
edges,
} = store.getState()
const nodes = getNodes()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
let startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const currentNode = nodes.find(node => node.id === nodeId)
if (currentNode?.parentId)
startNode = nodes.find(node => node.parentId === currentNode.parentId && node.data.isIterationStart)
if (!startNode)
return []
@ -145,21 +169,31 @@ export const useWorkflow = () => {
})
}, [store])
const getBeforeNodesInSameBranch = useCallback((nodeId: string) => {
const getBeforeNodesInSameBranch = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
const {
getNodes,
edges,
} = store.getState()
const nodes = getNodes()
const nodes = newNodes || getNodes()
const currentNode = nodes.find(node => node.id === nodeId)
const list: Node[] = []
if (!currentNode)
return list
if (currentNode.parentId) {
const parentNode = nodes.find(node => node.id === currentNode.parentId)
if (parentNode) {
const parentList = getBeforeNodesInSameBranch(parentNode.id)
list.push(...parentList)
}
}
const traverse = (root: Node, callback: (node: Node) => void) => {
if (root) {
const incomers = getIncomers(root, nodes, edges)
const incomers = getIncomers(root, nodes, newEdges || edges)
if (incomers.length) {
incomers.forEach((node) => {
@ -185,6 +219,21 @@ export const useWorkflow = () => {
return []
}, [store])
const getBeforeNodesInSameBranchIncludeParent = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
const nodes = getBeforeNodesInSameBranch(nodeId, newNodes, newEdges)
const {
getNodes,
} = store.getState()
const allNodes = getNodes()
const node = allNodes.find(n => n.id === nodeId)
const parentNodeId = node?.parentId
const parentNode = allNodes.find(n => n.id === parentNodeId)
if (parentNode)
nodes.push(parentNode)
return nodes
}, [getBeforeNodesInSameBranch, store])
const getAfterNodesInSameBranch = useCallback((nodeId: string) => {
const {
getNodes,
@ -227,11 +276,19 @@ export const useWorkflow = () => {
return getIncomers(node, nodes, edges)
}, [store])
const getIterationNodeChildren = useCallback((nodeId: string) => {
const {
getNodes,
} = store.getState()
const nodes = getNodes()
return nodes.filter(node => node.parentId === nodeId)
}, [store])
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
const { getNodes, setNodes } = store.getState()
const afterNodes = getAfterNodesInSameBranch(nodeId)
const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
// console.log(effectNodes)
if (effectNodes.length > 0) {
const newNodes = getNodes().map((node) => {
if (effectNodes.find(n => n.id === node.id))
@ -285,9 +342,13 @@ export const useWorkflow = () => {
const sourceNode: Node = nodes.find(node => node.id === source)!
const targetNode: Node = nodes.find(node => node.id === target)!
if (targetNode.data.isIterationStart)
return false
if (sourceNode && targetNode) {
const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes
const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start]
if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type))
return false
@ -338,6 +399,7 @@ export const useWorkflow = () => {
handleLayout,
getTreeLeafNodes,
getBeforeNodesInSameBranch,
getBeforeNodesInSameBranchIncludeParent,
getAfterNodesInSameBranch,
handleOutVarRenameChange,
isVarUsedInNodes,
@ -347,6 +409,7 @@ export const useWorkflow = () => {
formatTimeFromNow,
getNode,
getBeforeNodeById,
getIterationNodeChildren,
enableShortcuts,
disableShortcuts,
}
@ -370,6 +433,13 @@ export const useFetchToolsData = () => {
customTools: customTools || [],
})
}
if (type === 'workflow') {
const workflowTools = await fetchAllWorkflowTools()
workflowStore.setState({
workflowTools: workflowTools || [],
})
}
}, [workflowStore])
return {
@ -448,11 +518,14 @@ export const useWorkflowInit = () => {
handleFetchPreloadData()
handleFetchAllTools('builtin')
handleFetchAllTools('custom')
handleFetchAllTools('workflow')
}, [handleFetchPreloadData, handleFetchAllTools])
useEffect(() => {
if (data)
if (data) {
workflowStore.getState().setDraftUpdatedAt(data.updated_at)
workflowStore.getState().setToolPublished(data.tool_published)
}
}, [data, workflowStore])
return {
@ -499,14 +572,42 @@ export const useNodesReadOnly = () => {
export const useToolIcon = (data: Node['data']) => {
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const toolIcon = useMemo(() => {
if (data.type === BlockEnum.Tool) {
if (data.provider_type === 'builtin')
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon
let targetTools = buildInTools
if (data.provider_type === CollectionType.builtIn)
targetTools = buildInTools
else if (data.provider_type === CollectionType.custom)
targetTools = customTools
else
targetTools = workflowTools
return targetTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon
}
}, [data, buildInTools, customTools])
}, [data, buildInTools, customTools, workflowTools])
return toolIcon
}
export const useIsNodeInIteration = (iterationId: string) => {
const store = useStoreApi()
const isNodeInIteration = useCallback((nodeId: string) => {
const {
getNodes,
} = store.getState()
const nodes = getNodes()
const node = nodes.find(node => node.id === nodeId)
if (!node)
return false
if (node.parentId === iterationId)
return true
return false
}, [iterationId, store])
return {
isNodeInIteration,
}
}

View File

@ -64,11 +64,15 @@ import {
initialEdges,
initialNodes,
} from './utils'
import { WORKFLOW_DATA_UPDATE } from './constants'
import {
ITERATION_CHILDREN_Z_INDEX,
WORKFLOW_DATA_UPDATE,
} from './constants'
import Loading from '@/app/components/base/loading'
import { FeaturesProvider } from '@/app/components/base/features'
import type { Features as FeaturesData } from '@/app/components/base/features/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import Confirm from '@/app/components/base/confirm/common'
const nodeTypes = {
custom: CustomNode,
@ -94,6 +98,8 @@ const Workflow: FC<WorkflowProps> = memo(({
const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
const controlMode = useStore(s => s.controlMode)
const nodeAnimation = useStore(s => s.nodeAnimation)
const showConfirm = useStore(s => s.showConfirm)
const { setShowConfirm } = workflowStore.getState()
const {
handleSyncWorkflowDraft,
syncWorkflowDraftWhenPageClose,
@ -227,6 +233,18 @@ const Workflow: FC<WorkflowProps> = memo(({
<PanelContextmenu />
<NodeContextmenu />
<HelpLine />
{
!!showConfirm && (
<Confirm
isShow
onCancel={() => setShowConfirm(undefined)}
onConfirm={showConfirm.onConfirm}
title={showConfirm.title}
desc={showConfirm.desc}
confirmWrapperClassName='!z-[11]'
/>
)
}
<ReactFlow
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
@ -250,6 +268,7 @@ const Workflow: FC<WorkflowProps> = memo(({
onSelectionDrag={handleSelectionDrag}
onPaneContextMenu={handlePaneContextMenu}
connectionLineComponent={CustomConnectionLine}
connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }}
defaultViewport={viewport}
multiSelectionKeyCode={null}
deleteKeyCode={null}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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

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

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

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

View 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.
}

View 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

View 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,
}
}

View File

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

View File

@ -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#}}') && (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -180,6 +180,8 @@ export const useChat = (
isAnswer: true,
}
let isInIteration = false
handleResponding(true)
const bodyParams = {
@ -297,10 +299,11 @@ export const useChat = (
}
}))
},
onNodeStarted: ({ data }) => {
onIterationStart: ({ data }) => {
responseItem.workflowProcess!.tracing!.push({
...data,
status: NodeRunningStatus.Running,
details: [],
} as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
@ -309,22 +312,91 @@ export const useChat = (
...responseItem,
}
}))
isInIteration = true
},
onNodeFinished: ({ data }) => {
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
responseItem.workflowProcess!.tracing[currentIndex] = {
...(responseItem.workflowProcess!.tracing[currentIndex].extras
? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras }
: {}),
onIterationNext: () => {
const tracing = responseItem.workflowProcess!.tracing!
const iterations = tracing[tracing.length - 1]
iterations.details!.push([])
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.length - 1
draft[currentIndex] = responseItem
}))
},
onIterationFinish: ({ data }) => {
const tracing = responseItem.workflowProcess!.tracing!
const iterations = tracing[tracing.length - 1]
tracing[tracing.length - 1] = {
...iterations,
...data,
status: NodeRunningStatus.Succeeded,
} as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
const currentIndex = draft.length - 1
draft[currentIndex] = responseItem
}))
isInIteration = false
},
onNodeStarted: ({ data }) => {
if (isInIteration) {
const tracing = responseItem.workflowProcess!.tracing!
const iterations = tracing[tracing.length - 1]
const currIteration = iterations.details![iterations.details!.length - 1]
currIteration.push({
...data,
status: NodeRunningStatus.Running,
} as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.length - 1
draft[currentIndex] = responseItem
}))
}
else {
responseItem.workflowProcess!.tracing!.push({
...data,
status: NodeRunningStatus.Running,
} as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
}
},
onNodeFinished: ({ data }) => {
if (isInIteration) {
const tracing = responseItem.workflowProcess!.tracing!
const iterations = tracing[tracing.length - 1]
const currIteration = iterations.details![iterations.details!.length - 1]
currIteration[currIteration.length - 1] = {
...data,
status: NodeRunningStatus.Succeeded,
} as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.length - 1
draft[currentIndex] = responseItem
}))
}
else {
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
responseItem.workflowProcess!.tracing[currentIndex] = {
...(responseItem.workflowProcess!.tracing[currentIndex].extras
? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras }
: {}),
...data,
} as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
}
},
},
)

View File

@ -28,11 +28,12 @@ const Panel: FC = () => {
enableShortcuts,
disableShortcuts,
} = useWorkflow()
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal } = useAppStore(useShallow(state => ({
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem,
setCurrentLogItem: state.setCurrentLogItem,
showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal,
currentLogModalActiveTab: state.currentLogModalActiveTab,
})))
return (
@ -55,6 +56,7 @@ const Panel: FC = () => {
setCurrentLogItem()
setShowMessageLogModal(false)
}}
defaultTab={currentLogModalActiveTab}
/>
)
}

View File

@ -46,7 +46,7 @@ const InputsPanel = ({ onRun }: Props) => {
{
type: InputVarType.files,
variable: '__image',
required: true,
required: false,
label: 'files',
},
]

View File

@ -1,5 +1,6 @@
import {
memo,
useCallback,
useEffect,
// useRef,
useState,
@ -7,6 +8,7 @@ import {
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import copy from 'copy-to-clipboard'
import { useBoolean } from 'ahooks'
import ResultText from '../run/result-text'
import ResultPanel from '../run/result-panel'
import TracingPanel from '../run/tracing-panel'
@ -19,12 +21,18 @@ import {
} from '../types'
import { SimpleBtn } from '../../app/text-generate/item'
import Toast from '../../base/toast'
import IterationResultPanel from '../run/iteration-result-panel'
import InputsPanel from './inputs-panel'
import Loading from '@/app/components/base/loading'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
import { Clipboard } from '@/app/components/base/icons/src/vender/line/files'
import type { NodeTracing } from '@/types/workflow'
const WorkflowPreview = () => {
const WorkflowPreview = ({
onShowIterationDetail,
}: {
onShowIterationDetail: (detail: NodeTracing[][]) => void
}) => {
const { t } = useTranslation()
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const workflowRunningData = useStore(s => s.workflowRunningData)
@ -46,6 +54,31 @@ const WorkflowPreview = () => {
switchTab('DETAIL')
}, [workflowRunningData])
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [isShowIterationDetail, {
setTrue: doShowIterationDetail,
setFalse: doHideIterationDetail,
}] = useBoolean(false)
const handleShowIterationDetail = useCallback((detail: NodeTracing[][]) => {
setIterationRunResult(detail)
doShowIterationDetail()
}, [doShowIterationDetail])
if (isShowIterationDetail) {
return (
<div className={`
flex flex-col w-[420px] h-full rounded-l-2xl border-[0.5px] border-gray-200 shadow-xl bg-white
`}>
<IterationResultPanel
list={iterationRunResult}
onHide={doHideIterationDetail}
onBack={doHideIterationDetail}
/>
</div>
)
}
return (
<div className={`
flex flex-col w-[420px] h-full rounded-l-2xl border-[0.5px] border-gray-200 shadow-xl bg-white
@ -57,115 +90,129 @@ const WorkflowPreview = () => {
</div>
</div>
<div className='grow relative flex flex-col'>
<div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
{showInputsPanel && (
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'INPUT' && '!border-[rgb(21,94,239)] text-gray-700',
)}
onClick={() => switchTab('INPUT')}
>{t('runLog.input')}</div>
)}
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
if (!workflowRunningData)
return
switchTab('RESULT')
}}
>{t('runLog.result')}</div>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-gray-700',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
if (!workflowRunningData)
return
switchTab('DETAIL')
}}
>{t('runLog.detail')}</div>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-gray-700',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
if (!workflowRunningData)
return
switchTab('TRACING')
}}
>{t('runLog.tracing')}</div>
</div>
<div className={cn(
'grow bg-white h-0 overflow-y-auto rounded-b-2xl',
(currentTab === 'RESULT' || currentTab === 'TRACING') && '!bg-gray-50',
)}>
{currentTab === 'INPUT' && showInputsPanel && (
<InputsPanel onRun={() => switchTab('RESULT')} />
)}
{currentTab === 'RESULT' && (
{isShowIterationDetail
? (
<IterationResultPanel
list={iterationRunResult}
onHide={doHideIterationDetail}
onBack={doHideIterationDetail}
/>
)
: (
<>
<ResultText
isRunning={workflowRunningData?.result?.status === WorkflowRunningStatus.Running || !workflowRunningData?.result}
outputs={workflowRunningData?.resultText}
error={workflowRunningData?.result?.error}
onClick={() => switchTab('DETAIL')}
/>
{(workflowRunningData?.result.status !== WorkflowRunningStatus.Succeeded || !workflowRunningData?.resultText) && (
<SimpleBtn
className={cn('ml-4 mb-4 inline-flex space-x-1')}
<div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
{showInputsPanel && (
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'INPUT' && '!border-[rgb(21,94,239)] text-gray-700',
)}
onClick={() => switchTab('INPUT')}
>{t('runLog.input')}</div>
)}
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
const content = workflowRunningData?.resultText
if (typeof content === 'string')
copy(content)
else
copy(JSON.stringify(content))
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<Clipboard className='w-3.5 h-3.5' />
<div>{t('common.operation.copy')}</div>
</SimpleBtn>
)}
if (!workflowRunningData)
return
switchTab('RESULT')
}}
>{t('runLog.result')}</div>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-gray-700',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
if (!workflowRunningData)
return
switchTab('DETAIL')
}}
>{t('runLog.detail')}</div>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-gray-700',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
if (!workflowRunningData)
return
switchTab('TRACING')
}}
>{t('runLog.tracing')}</div>
</div>
<div className={cn(
'grow bg-white h-0 overflow-y-auto rounded-b-2xl',
(currentTab === 'RESULT' || currentTab === 'TRACING') && '!bg-gray-50',
)}>
{currentTab === 'INPUT' && showInputsPanel && (
<InputsPanel onRun={() => switchTab('RESULT')} />
)}
{currentTab === 'RESULT' && (
<>
<ResultText
isRunning={workflowRunningData?.result?.status === WorkflowRunningStatus.Running || !workflowRunningData?.result}
outputs={workflowRunningData?.resultText}
error={workflowRunningData?.result?.error}
onClick={() => switchTab('DETAIL')}
/>
{(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData?.resultText === 'string') && (
<SimpleBtn
className={cn('ml-4 mb-4 inline-flex space-x-1')}
onClick={() => {
const content = workflowRunningData?.resultText
if (typeof content === 'string')
copy(content)
else
copy(JSON.stringify(content))
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<Clipboard className='w-3.5 h-3.5' />
<div>{t('common.operation.copy')}</div>
</SimpleBtn>
)}
</>
)}
{currentTab === 'DETAIL' && (
<ResultPanel
inputs={workflowRunningData?.result?.inputs}
outputs={workflowRunningData?.result?.outputs}
status={workflowRunningData?.result?.status || ''}
error={workflowRunningData?.result?.error}
elapsed_time={workflowRunningData?.result?.elapsed_time}
total_tokens={workflowRunningData?.result?.total_tokens}
created_at={workflowRunningData?.result?.created_at}
created_by={(workflowRunningData?.result?.created_by as any)?.name}
steps={workflowRunningData?.result?.total_steps}
/>
)}
{currentTab === 'DETAIL' && !workflowRunningData?.result && (
<div className='flex h-full items-center justify-center bg-white'>
<Loading />
</div>
)}
{currentTab === 'TRACING' && (
<TracingPanel
list={workflowRunningData?.tracing || []}
onShowIterationDetail={handleShowIterationDetail}
/>
)}
{currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && (
<div className='flex h-full items-center justify-center bg-gray-50'>
<Loading />
</div>
)}
</div>
</>
)}
{currentTab === 'DETAIL' && (
<ResultPanel
inputs={workflowRunningData?.result?.inputs}
outputs={workflowRunningData?.result?.outputs}
status={workflowRunningData?.result?.status || ''}
error={workflowRunningData?.result?.error}
elapsed_time={workflowRunningData?.result?.elapsed_time}
total_tokens={workflowRunningData?.result?.total_tokens}
created_at={workflowRunningData?.result?.created_at}
created_by={(workflowRunningData?.result?.created_by as any)?.name}
steps={workflowRunningData?.result?.total_steps}
/>
)}
{currentTab === 'DETAIL' && !workflowRunningData?.result && (
<div className='flex h-full items-center justify-center bg-white'>
<Loading />
</div>
)}
{currentTab === 'TRACING' && (
<TracingPanel
list={workflowRunningData?.tracing || []}
/>
)}
{currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && (
<div className='flex h-full items-center justify-center bg-gray-50'>
<Loading />
</div>
)}
</div>
</div>
</div>
)

View File

@ -4,9 +4,12 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { useBoolean } from 'ahooks'
import { BlockEnum } from '../types'
import OutputPanel from './output-panel'
import ResultPanel from './result-panel'
import TracingPanel from './tracing-panel'
import IterationResultPanel from './iteration-result-panel'
import { ToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import { fetchRunDetail, fetchTracingList } from '@/service/log'
@ -19,9 +22,10 @@ export type RunProps = {
activeTab?: 'RESULT' | 'DETAIL' | 'TRACING'
runID: string
getResultCallback?: (result: WorkflowRunDetailResponse) => void
onShowIterationDetail: (detail: NodeTracing[][]) => void
}
const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getResultCallback }) => {
const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getResultCallback, onShowIterationDetail }) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [currentTab, setCurrentTab] = useState<string>(activeTab)
@ -56,12 +60,76 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
}
}, [notify, getResultCallback])
const formatNodeList = useCallback((list: NodeTracing[]) => {
const allItems = list.reverse()
const result: NodeTracing[] = []
let iterationIndexInfos: {
start: number
end: number
}[] = []
allItems.forEach((item) => {
const { node_type, index, execution_metadata } = item
if (node_type !== BlockEnum.Iteration) {
let isInIteration = false
let isIterationFirstNode = false
iterationIndexInfos.forEach(({ start, end }) => {
if (index >= start && index < end) {
if (index === start)
isIterationFirstNode = true
isInIteration = true
}
})
if (isInIteration) {
const iterationDetails = result[result.length - 1].details!
if (isIterationFirstNode)
iterationDetails!.push([item])
else
iterationDetails[iterationDetails.length - 1].push(item)
return
}
// not in iteration
result.push(item)
return
}
const { steps_boundary } = execution_metadata
iterationIndexInfos = []
steps_boundary.forEach((boundary, index) => {
if (index === 0) {
iterationIndexInfos.push({
start: boundary,
end: 0,
})
}
else if (index === steps_boundary.length - 1) {
iterationIndexInfos[iterationIndexInfos.length - 1].end = boundary
}
else {
iterationIndexInfos[iterationIndexInfos.length - 1].end = boundary
iterationIndexInfos.push({
start: boundary,
end: 0,
})
}
})
result.push({
...item,
details: [],
})
})
return result
}, [])
const getTracingList = useCallback(async (appID: string, runID: string) => {
try {
const { data: nodeList } = await fetchTracingList({
url: `/apps/${appID}/workflow-runs/${runID}/node-executions`,
})
setList(nodeList.reverse())
setList(formatNodeList(nodeList))
}
catch (err) {
notify({
@ -103,6 +171,29 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
adjustResultHeight()
}, [loading])
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [isShowIterationDetail, {
setTrue: doShowIterationDetail,
setFalse: doHideIterationDetail,
}] = useBoolean(false)
const handleShowIterationDetail = useCallback((detail: NodeTracing[][]) => {
setIterationRunResult(detail)
doShowIterationDetail()
}, [doShowIterationDetail])
if (isShowIterationDetail) {
return (
<div className='grow relative flex flex-col'>
<IterationResultPanel
list={iterationRunResult}
onHide={doHideIterationDetail}
onBack={doHideIterationDetail}
/>
</div>
)
}
return (
<div className='grow relative flex flex-col'>
{/* tab */}
@ -161,6 +252,7 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
{!loading && currentTab === 'TRACING' && (
<TracingPanel
list={list}
onShowIterationDetail={handleShowIterationDetail}
/>
)}
</div>

View File

@ -0,0 +1,92 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows'
import NodePanel from './node'
import type { NodeTracing } from '@/types/workflow'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
const i18nPrefix = 'workflow.singleRun'
type Props = {
list: NodeTracing[][]
onHide: () => void
onBack: () => void
noWrap?: boolean
}
const IterationResultPanel: FC<Props> = ({
list,
onHide,
onBack,
noWrap,
}) => {
const { t } = useTranslation()
const main = (
<>
<div className={cn(!noWrap && 'shrink-0 ', 'pl-4 pr-3 pt-3')}>
<div className='shrink-0 flex justify-between items-center h-8'>
<div className='text-base font-semibold text-gray-900 truncate'>
{t(`${i18nPrefix}.testRunIteration`)}
</div>
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}>
<XClose className='w-4 h-4 text-gray-500 ' />
</div>
</div>
<div className='flex items-center py-2 space-x-1 text-primary-600 cursor-pointer' onClick={onBack}>
<ArrowNarrowLeft className='w-4 h-4' />
<div className='leading-[18px] text-[13px] font-medium'>{t(`${i18nPrefix}.back`)}</div>
</div>
</div>
{/* List */}
<div className={cn(!noWrap ? 'h-0 grow' : 'max-h-full', 'overflow-y-auto px-4 pb-4 bg-gray-50')}>
{list.map((iteration, index) => (
<div key={index} className={cn('my-4', index === 0 && 'mt-2')}>
<div className='flex items-center'>
<div className='shrink-0 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t(`${i18nPrefix}.iteration`)} {index + 1}</div>
<div
className='ml-3 grow w-0 h-px'
style={{ background: 'linear-gradient(to right, #F3F4F6, rgba(243, 244, 246, 0))' }}
></div>
</div>
<div className='mt-0.5 space-y-1'>
{iteration.map(node => (
<NodePanel
key={node.id}
className='!px-0 !py-0'
nodeInfo={node}
notShowIterationNav
/>
))}
</div>
</div>
))}
</div>
</>
)
const handleNotBubble = useCallback((e: React.MouseEvent) => {
// if not do this, it will trigger the message log modal disappear(useClickAway)
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}, [])
if (noWrap)
return main
return (
<div
className='absolute inset-0 z-10 rounded-2xl pt-10'
style={{
backgroundColor: 'rgba(16, 24, 40, 0.20)',
}}
onClick={handleNotBubble}
>
<div className='h-full rounded-2xl bg-white flex flex-col'>
{main}
</div>
</div >
)
}
export default React.memo(IterationResultPanel)

View File

@ -4,23 +4,33 @@ import type { FC } from 'react'
import { useCallback, useEffect, useState } from 'react'
import cn from 'classnames'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import Split from '../nodes/_base/components/split'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { AlertCircle, AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import { CheckCircle, Loading02 } from '@/app/components/base/icons/src/vender/line/general'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { ArrowNarrowRight, ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import type { NodeTracing } from '@/types/workflow'
type Props = {
className?: string
nodeInfo: NodeTracing
hideInfo?: boolean
hideProcessDetail?: boolean
onShowIterationDetail?: (detail: NodeTracing[][]) => void
notShowIterationNav?: boolean
justShowIterationNavArrow?: boolean
}
const NodePanel: FC<Props> = ({
className,
nodeInfo,
hideInfo = false,
hideProcessDetail,
onShowIterationDetail,
notShowIterationNav,
justShowIterationNavArrow,
}) => {
const [collapseState, doSetCollapseState] = useState<boolean>(true)
const setCollapseState = useCallback((state: boolean) => {
@ -51,8 +61,14 @@ const NodePanel: FC<Props> = ({
setCollapseState(!nodeInfo.expand)
}, [nodeInfo.expand, setCollapseState])
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowIterationDetail?.(nodeInfo.details || [])
}
return (
<div className={cn('px-4 py-1', hideInfo && '!p-0')}>
<div className={cn('px-4 py-1', className, hideInfo && '!p-0')}>
<div className={cn('group transition-all bg-white border border-gray-100 rounded-2xl shadow-xs hover:shadow-md', hideInfo && '!rounded-lg')}>
<div
className={cn(
@ -97,6 +113,27 @@ const NodePanel: FC<Props> = ({
</div>
{!collapseState && !hideProcessDetail && (
<div className='pb-2'>
{/* The nav to the iteration detail */}
{isIterationNode && !notShowIterationNav && (
<div className='mt-2 mb-1 !px-2'>
<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={handleOnShowIterationDetail}>
<div className='leading-[18px] text-[13px] font-medium text-gray-700'>{t('workflow.nodes.iteration.iteration', { count: nodeInfo.metadata?.iterator_length || (nodeInfo.execution_metadata?.steps_boundary?.length - 1) })}</div>
{justShowIterationNavArrow
? (
<ArrowNarrowRight className='w-3.5 h-3.5 text-gray-500' />
)
: (
<div className='flex items-center space-x-1 text-[#155EEF]'>
<div className='text-[13px] font-normal '>{t('workflow.common.viewDetailInTracingPanel')}</div>
<ArrowNarrowRight className='w-3.5 h-3.5' />
</div>
)}
</div>
<Split className='mt-2' />
</div>
)}
<div className={cn('px-[10px] py-1', hideInfo && '!px-2 !py-0.5')}>
{nodeInfo.status === 'stopped' && (
<div className='px-3 py-[10px] bg-[#fffaeb] rounded-lg border-[0.5px] border-[rbga(0,0,0,0.05)] text-xs leading-[18px] text-[#dc6803] shadow-xs'>{t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}</div>

View File

@ -5,15 +5,18 @@ import type { NodeTracing } from '@/types/workflow'
type TracingPanelProps = {
list: NodeTracing[]
onShowIterationDetail: (detail: NodeTracing[][]) => void
}
const TracingPanel: FC<TracingPanelProps> = ({ list }) => {
const TracingPanel: FC<TracingPanelProps> = ({ list, onShowIterationDetail }) => {
return (
<div className='bg-gray-50 py-2'>
{list.map(node => (
<NodePanel
key={node.id}
nodeInfo={node}
onShowIterationDetail={onShowIterationDetail}
justShowIterationNavArrow
/>
))}
</div>

View File

@ -9,6 +9,7 @@ import type {
HelpLineHorizontalPosition,
HelpLineVerticalPosition,
} from './help-line/types'
import type { VariableAssignerNodeType } from './nodes/variable-assigner/types'
import type {
Edge,
HistoryWorkflowData,
@ -47,6 +48,8 @@ type Shape = {
setShowInputsPanel: (showInputsPanel: boolean) => void
inputs: Record<string, string>
setInputs: (inputs: Record<string, string>) => void
toolPublished: boolean
setToolPublished: (toolPublished: boolean) => void
files: RunFile[]
setFiles: (files: RunFile[]) => void
backupDraft?: {
@ -69,6 +72,8 @@ type Shape = {
setBuildInTools: (tools: ToolWithProvider[]) => void
customTools: ToolWithProvider[]
setCustomTools: (tools: ToolWithProvider[]) => void
workflowTools: ToolWithProvider[]
setWorkflowTools: (tools: ToolWithProvider[]) => void
clipboardElements: Node[]
setClipboardElements: (clipboardElements: Node[]) => void
shortcutsDisabled: boolean
@ -98,6 +103,25 @@ type Shape = {
setMousePosition: (mousePosition: Shape['mousePosition']) => void
syncWorkflowDraftHash: string
setSyncWorkflowDraftHash: (hash: string) => void
showConfirm?: { title: string; desc?: string; onConfirm: () => void }
setShowConfirm: (showConfirm: Shape['showConfirm']) => void
showAssignVariablePopup?: {
nodeId: string
nodeData: Node['data']
variableAssignerNodeId: string
variableAssignerNodeData: VariableAssignerNodeType
variableAssignerNodeHandleId: string
parentNode?: Node
x: number
y: number
}
setShowAssignVariablePopup: (showAssignVariablePopup: Shape['showAssignVariablePopup']) => void
hoveringAssignVariableGroupId?: string
setHoveringAssignVariableGroupId: (hoveringAssignVariableGroupId?: string) => void
connectingNodePayload?: { nodeId: string; nodeType: string; handleType: string; handleId: string | null }
setConnectingNodePayload: (startConnectingPayload?: Shape['connectingNodePayload']) => void
enteringNodePayload?: { nodeId: string }
setEnteringNodePayload: (enteringNodePayload?: Shape['enteringNodePayload']) => void
}
export const createWorkflowStore = () => {
@ -124,6 +148,8 @@ export const createWorkflowStore = () => {
setShowInputsPanel: showInputsPanel => set(() => ({ showInputsPanel })),
inputs: {},
setInputs: inputs => set(() => ({ inputs })),
toolPublished: false,
setToolPublished: toolPublished => set(() => ({ toolPublished })),
files: [],
setFiles: files => set(() => ({ files })),
backupDraft: undefined,
@ -143,6 +169,8 @@ export const createWorkflowStore = () => {
setBuildInTools: buildInTools => set(() => ({ buildInTools })),
customTools: [],
setCustomTools: customTools => set(() => ({ customTools })),
workflowTools: [],
setWorkflowTools: workflowTools => set(() => ({ workflowTools })),
clipboardElements: [],
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
shortcutsDisabled: false,
@ -168,6 +196,16 @@ export const createWorkflowStore = () => {
setMousePosition: mousePosition => set(() => ({ mousePosition })),
syncWorkflowDraftHash: '',
setSyncWorkflowDraftHash: syncWorkflowDraftHash => set(() => ({ syncWorkflowDraftHash })),
showConfirm: undefined,
setShowConfirm: showConfirm => set(() => ({ showConfirm })),
showAssignVariablePopup: undefined,
setShowAssignVariablePopup: showAssignVariablePopup => set(() => ({ showAssignVariablePopup })),
hoveringAssignVariableGroupId: undefined,
setHoveringAssignVariableGroupId: hoveringAssignVariableGroupId => set(() => ({ hoveringAssignVariableGroupId })),
connectingNodePayload: undefined,
setConnectingNodePayload: connectingNodePayload => set(() => ({ connectingNodePayload })),
enteringNodePayload: undefined,
setEnteringNodePayload: enteringNodePayload => set(() => ({ enteringNodePayload })),
}))
}

View File

@ -21,7 +21,10 @@ export enum BlockEnum {
TemplateTransform = 'template-transform',
HttpRequest = 'http-request',
VariableAssigner = 'variable-assigner',
VariableAggregator = 'variable-aggregator',
Tool = 'tool',
ParameterExtractor = 'parameter-extractor',
Iteration = 'iteration',
}
export type Branch = {
@ -30,7 +33,6 @@ export type Branch = {
}
export type CommonNodeType<T = {}> = {
_isInvalidConnection?: boolean
_connectedSourceHandleIds?: string[]
_connectedTargetHandleIds?: string[]
_targetBranches?: Branch[]
@ -39,10 +41,21 @@ export type CommonNodeType<T = {}> = {
_singleRunningStatus?: NodeRunningStatus
_isCandidate?: boolean
_isBundled?: boolean
_children?: string[]
_isEntering?: boolean
_showAddVariablePopup?: boolean
_holdAddVariablePopup?: boolean
_iterationLength?: number
_iterationIndex?: number
isIterationStart?: boolean
isInIteration?: boolean
iteration_id?: string
selected?: boolean
title: string
desc: string
type: BlockEnum
width?: number
height?: number
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
export type CommonEdgeType = {
@ -51,6 +64,8 @@ export type CommonEdgeType = {
_connectedNodeIsSelected?: boolean
_runned?: boolean
_isBundled?: boolean
isInIteration?: boolean
iteration_id?: string
sourceType: BlockEnum
targetType: BlockEnum
}
@ -101,6 +116,7 @@ export enum InputVarType {
files = 'files',
json = 'json', // obj, array
contexts = 'contexts', // knowledge retrieval
iterator = 'iterator', // iteration input
}
export type InputVar = {
@ -173,6 +189,7 @@ export enum VarType {
arrayNumber = 'array[number]',
arrayObject = 'array[object]',
arrayFile = 'array[file]',
any = 'any',
}
export type Var = {

View File

@ -3,7 +3,7 @@ import {
getConnectedEdges,
getOutgoers,
} from 'reactflow'
import dagre from 'dagre'
import dagre from '@dagrejs/dagre'
import { v4 as uuid4 } from 'uuid'
import {
cloneDeep,
@ -17,6 +17,7 @@ import type {
} from './types'
import { BlockEnum } from './types'
import {
ITERATION_NODE_Z_INDEX,
NODE_WIDTH_X_OFFSET,
START_INITIAL_POSITION,
} from './constants'
@ -93,6 +94,16 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
})
}
const iterationNodeMap = nodes.reduce((acc, node) => {
if (node.parentId) {
if (acc[node.parentId])
acc[node.parentId].push(node.id)
else
acc[node.parentId] = [node.id]
}
return acc
}, {} as Record<string, string[]>)
return nodes.map((node) => {
node.type = 'custom'
@ -119,6 +130,9 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
})
}
if (node.data.type === BlockEnum.Iteration)
node.data._children = iterationNodeMap[node.id] || []
return node
})
}
@ -172,19 +186,25 @@ export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
})
}
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
const nodes = cloneDeep(originNodes)
const edges = cloneDeep(originEdges)
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
const nodes = cloneDeep(originNodes).filter(node => !node.parentId)
const edges = cloneDeep(originEdges).filter(edge => !edge.data?.isInIteration)
dagreGraph.setGraph({
rankdir: 'LR',
align: 'UL',
nodesep: 40,
ranksep: 60,
ranker: 'tight-tree',
marginx: 30,
marginy: 200,
})
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: node.width, height: node.height })
dagreGraph.setNode(node.id, {
width: node.width!,
height: node.height!,
})
})
edges.forEach((edge) => {
@ -204,6 +224,8 @@ export const canRunBySingle = (nodeType: BlockEnum) => {
|| nodeType === BlockEnum.QuestionClassifier
|| nodeType === BlockEnum.HttpRequest
|| nodeType === BlockEnum.Tool
|| nodeType === BlockEnum.ParameterExtractor
|| nodeType === BlockEnum.Iteration
}
type ConnectedSourceOrTargetNodesChange = {
@ -254,7 +276,7 @@ export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSo
return nodesConnectedSourceOrTargetHandleIdsMap
}
export const generateNewNode = ({ data, position, id }: Pick<Node, 'data' | 'position'> & { id?: string }) => {
export const generateNewNode = ({ data, position, id, zIndex, ...rest }: Omit<Node, 'id'> & { id?: string }) => {
return {
id: id || `${Date.now()}`,
type: 'custom',
@ -262,6 +284,8 @@ export const generateNewNode = ({ data, position, id }: Pick<Node, 'data' | 'pos
position,
targetPosition: Position.Left,
sourcePosition: Position.Right,
zIndex: data.type === BlockEnum.Iteration ? ITERATION_NODE_Z_INDEX : zIndex,
...rest,
} as Node
}
@ -287,11 +311,15 @@ export const getValidTreeNodes = (nodes: Node[], edges: Edge[]) => {
if (outgoers.length) {
outgoers.forEach((outgoer) => {
list.push(outgoer)
if (outgoer.data.type === BlockEnum.Iteration)
list.push(...nodes.filter(node => node.parentId === outgoer.id))
traverse(outgoer, depth + 1)
})
}
else {
list.push(root)
if (root.data.type === BlockEnum.Iteration)
list.push(...nodes.filter(node => node.parentId === root.id))
}
}
@ -307,11 +335,12 @@ export const getToolCheckParams = (
toolData: ToolNodeType,
buildInTools: ToolWithProvider[],
customTools: ToolWithProvider[],
workflowTools: ToolWithProvider[],
language: string,
) => {
const { provider_id, provider_type, tool_name } = toolData
const isBuiltIn = provider_type === CollectionType.builtIn
const currentTools = isBuiltIn ? buildInTools : customTools
const currentTools = provider_type === CollectionType.builtIn ? buildInTools : provider_type === CollectionType.custom ? customTools : workflowTools
const currCollection = currentTools.find(item => item.id === provider_id)
const currTool = currCollection?.tools.find(tool => tool.name === tool_name)
const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : []