diff --git a/web/app/components/base/icons/assets/vender/workflow/human-in-loop.svg b/web/app/components/base/icons/assets/vender/workflow/human-in-loop.svg new file mode 100644 index 0000000000..07efc8ecc6 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/workflow/human-in-loop.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/src/vender/workflow/HumanInLoop.json b/web/app/components/base/icons/src/vender/workflow/HumanInLoop.json new file mode 100644 index 0000000000..ac23ac7a68 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/HumanInLoop.json @@ -0,0 +1,48 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M3.66634 2.66675C3.66634 2.29856 3.36787 2.00008 2.99967 2.00008C2.63149 2.00008 2.33301 2.29855 2.33301 2.66675L3.66634 2.66675ZM2.99967 5.33341H2.33301C2.33301 5.51023 2.40325 5.6798 2.52827 5.80482C2.65329 5.92984 2.82286 6.00008 2.99967 6.00008V5.33341ZM5.66641 6.00008C6.03459 6.00008 6.33307 5.7016 6.33307 5.33341C6.33307 4.96523 6.03459 4.66675 5.66641 4.66675L5.66641 6.00008ZM2.41183 5.01659C2.23816 5.34125 2.36056 5.74523 2.68522 5.91889C3.00988 6.09256 3.41385 5.97016 3.58752 5.6455L2.41183 5.01659ZM3.03395 8.58915C2.99109 8.22348 2.6599 7.96175 2.29421 8.00461C1.92853 8.04748 1.66682 8.37868 1.70967 8.74435L3.03395 8.58915ZM12.0439 5.05931C12.2607 5.3569 12.6777 5.42238 12.9753 5.20557C13.2729 4.98876 13.3383 4.57176 13.1215 4.27417L12.0439 5.05931ZM5.02145 13.5907C5.34627 13.7641 5.75013 13.6413 5.92349 13.3165C6.09685 12.9917 5.97407 12.5878 5.64925 12.4145L5.02145 13.5907ZM2.33301 2.66675L2.33301 5.33341H3.66634L3.66634 2.66675L2.33301 2.66675ZM2.99967 6.00008L5.66641 6.00008L5.66641 4.66675H2.99967L2.99967 6.00008ZM3.58752 5.6455C4.43045 4.06972 6.09066 3.00008 7.99968 3.00008V1.66675C5.57951 1.66675 3.47747 3.02445 2.41183 5.01659L3.58752 5.6455ZM7.99968 3.00008C9.66128 3.00008 11.1336 3.80991 12.0439 5.05931L13.1215 4.27417C11.9711 2.69513 10.1055 1.66675 7.99968 1.66675V3.00008ZM5.64925 12.4145C4.23557 11.6599 3.22828 10.2474 3.03395 8.58915L1.70967 8.74435C1.95639 10.8495 3.23403 12.6367 5.02145 13.5907L5.64925 12.4145Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M13.1946 8.13826C12.9027 7.84637 12.4294 7.84638 12.1375 8.13829L9.11842 11.1574C9.01642 11.2594 8.95025 11.3917 8.92985 11.5345C8.92985 11.5345 8.92985 11.5345 8.92985 11.5345L8.78508 12.5479L9.79846 12.4031C9.94127 12.3827 10.0736 12.3165 10.1756 12.2145L13.1947 9.19548C13.4866 8.90359 13.4866 8.43027 13.1946 8.13826C13.1947 8.13827 13.1946 8.13825 13.1946 8.13826ZM11.1947 7.19548C12.0073 6.38286 13.3249 6.38286 14.1375 7.19548C14.95 8.00814 14.9501 9.32565 14.1375 10.1383L11.1184 13.1574C10.8124 13.4633 10.4154 13.6618 9.98703 13.723L8.09369 13.9935C7.88596 14.0232 7.67639 13.9533 7.52801 13.805C7.37963 13.6566 7.30977 13.447 7.33945 13.2393L7.60991 11.3459C7.67111 10.9175 7.86961 10.5205 8.17561 10.2145L11.1947 7.19548Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M12.528 10.8048L10.528 8.80482L11.4708 7.86201L13.4708 9.86201L12.528 10.8048Z", + "fill": "currentColor" + }, + "children": [] + } + ] + }, + "name": "HumanInLoop" +} diff --git a/web/app/components/base/icons/src/vender/workflow/HumanInLoop.tsx b/web/app/components/base/icons/src/vender/workflow/HumanInLoop.tsx new file mode 100644 index 0000000000..084073b332 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/HumanInLoop.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './HumanInLoop.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => + +Icon.displayName = 'HumanInLoop' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts index 61fbd4b21c..10c5fd8cc3 100644 --- a/web/app/components/base/icons/src/vender/workflow/index.ts +++ b/web/app/components/base/icons/src/vender/workflow/index.ts @@ -6,6 +6,7 @@ export { default as DocsExtractor } from './DocsExtractor' export { default as End } from './End' export { default as Home } from './Home' export { default as Http } from './Http' +export { default as HumanInLoop } from './HumanInLoop' export { default as IfElse } from './IfElse' export { default as IterationStart } from './IterationStart' export { default as Iteration } from './Iteration' diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index 1e76efc2aa..fd727dd38c 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -10,6 +10,7 @@ import { End, Home, Http, + HumanInLoop, IfElse, Iteration, KnowledgeRetrieval, @@ -60,6 +61,7 @@ const getIcon = (type: BlockEnum, className: string) => { [BlockEnum.DocExtractor]: , [BlockEnum.ListFilter]: , [BlockEnum.Agent]: , + [BlockEnum.HumanInput]: , }[type] } const ICON_CONTAINER_BG_COLOR_MAP: Record = { @@ -83,6 +85,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record = { [BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500', [BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500', [BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500', + [BlockEnum.HumanInput]: 'bg-util-colors-cyan-cyan-500', } const BlockIcon: FC = ({ type, diff --git a/web/app/components/workflow/block-selector/constants.tsx b/web/app/components/workflow/block-selector/constants.tsx index 680cbf45b9..589115d4b4 100644 --- a/web/app/components/workflow/block-selector/constants.tsx +++ b/web/app/components/workflow/block-selector/constants.tsx @@ -100,6 +100,11 @@ export const BLOCKS: Block[] = [ type: BlockEnum.Agent, title: 'Agent', }, + { + classification: BlockClassificationEnum.Default, + type: BlockEnum.HumanInput, + title: 'Human Input', + }, ] export const BLOCK_CLASSIFICATIONS: string[] = [ diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 0ef4dc9dea..a5045c867a 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -22,6 +22,7 @@ import IterationStartDefault from './nodes/iteration-start/default' import AgentDefault from './nodes/agent/default' import LoopStartDefault from './nodes/loop-start/default' import LoopEndDefault from './nodes/loop-end/default' +import HumanInputDefault from './nodes/human-input/default' type NodesExtraData = { author: string @@ -242,6 +243,15 @@ export const NODES_EXTRA_DATA: Record = { getAvailableNextNodes: ListFilterDefault.getAvailableNextNodes, checkValid: AgentDefault.checkValid, }, + [BlockEnum.HumanInput]: { + author: 'Dify', + about: '', + availablePrevNodes: [], + availableNextNodes: [], + getAvailablePrevNodes: HumanInputDefault.getAvailablePrevNodes, + getAvailableNextNodes: HumanInputDefault.getAvailableNextNodes, + checkValid: HumanInputDefault.checkValid, + }, } export const NODES_INITIAL_DATA = { @@ -401,6 +411,12 @@ export const NODES_INITIAL_DATA = { desc: '', ...AgentDefault.defaultValue, }, + [BlockEnum.HumanInput]: { + type: BlockEnum.HumanInput, + title: '', + desc: '', + ...HumanInputDefault.defaultValue, + }, } export const MAX_ITERATION_PARALLEL_NUM = 10 export const MIN_ITERATION_PARALLEL_NUM = 1 diff --git a/web/app/components/workflow/nodes/constants.ts b/web/app/components/workflow/nodes/constants.ts index 0cd6922233..ccd92b4aa8 100644 --- a/web/app/components/workflow/nodes/constants.ts +++ b/web/app/components/workflow/nodes/constants.ts @@ -20,6 +20,8 @@ import TemplateTransformNode from './template-transform/node' import TemplateTransformPanel from './template-transform/panel' import HttpNode from './http/node' import HttpPanel from './http/panel' +import HumanInputNode from './human-input/node' +import HumanInputPanel from './human-input/panel' import ToolNode from './tool/node' import ToolPanel from './tool/panel' import VariableAssignerNode from './variable-assigner/node' @@ -61,6 +63,7 @@ export const NodeComponentMap: Record> = { [BlockEnum.DocExtractor]: DocExtractorNode, [BlockEnum.ListFilter]: ListFilterNode, [BlockEnum.Agent]: AgentNode, + [BlockEnum.HumanInput]: HumanInputNode, } export const PanelComponentMap: Record> = { @@ -84,6 +87,7 @@ export const PanelComponentMap: Record> = { [BlockEnum.DocExtractor]: DocExtractorPanel, [BlockEnum.ListFilter]: ListFilterPanel, [BlockEnum.Agent]: AgentPanel, + [BlockEnum.HumanInput]: HumanInputPanel, } export const CUSTOM_NODE_TYPE = 'custom' diff --git a/web/app/components/workflow/nodes/human-input/default.ts b/web/app/components/workflow/nodes/human-input/default.ts new file mode 100644 index 0000000000..a82bc066a2 --- /dev/null +++ b/web/app/components/workflow/nodes/human-input/default.ts @@ -0,0 +1,28 @@ +import type { NodeDefault } from '../../types' +import type { HumanInputNodeType } from './types' +import { ALL_CHAT_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks' + +const nodeDefault: NodeDefault = { + defaultValue: { + deliveryMethod: [], + userActions: [], + }, + getAvailablePrevNodes(isChatMode: boolean) { + const nodes = isChatMode + ? ALL_CHAT_AVAILABLE_BLOCKS + : [] + return nodes + }, + getAvailableNextNodes() { + const nodes = ALL_CHAT_AVAILABLE_BLOCKS + return nodes + }, + checkValid() { + return { + isValid: true, + errorMessage: '', + } + }, +} + +export default nodeDefault diff --git a/web/app/components/workflow/nodes/human-input/node.tsx b/web/app/components/workflow/nodes/human-input/node.tsx new file mode 100644 index 0000000000..7681df594a --- /dev/null +++ b/web/app/components/workflow/nodes/human-input/node.tsx @@ -0,0 +1,34 @@ +import type { FC } from 'react' +import React from 'react' +import type { HumanInputNodeType } from './types' +import type { NodeProps } from '@/app/components/workflow/types' +// import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' +import { + useIsChatMode, + useWorkflow, + useWorkflowVariables, +} from '@/app/components/workflow/hooks' +// 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 { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' +// import { BlockEnum } from '@/app/components/workflow/types' +// import cn from 'classnames' + +const Node: FC> = ({ + id, + data, +}) => { + const { getBeforeNodesInSameBranch } = useWorkflow() + const availableNodes = getBeforeNodesInSameBranch(id) + const { getCurrentVariableType } = useWorkflowVariables() + const isChatMode = useIsChatMode() + + return ( +
+ TODO +
+ ) +} + +export default React.memo(Node) diff --git a/web/app/components/workflow/nodes/human-input/panel.tsx b/web/app/components/workflow/nodes/human-input/panel.tsx new file mode 100644 index 0000000000..3a115fed0b --- /dev/null +++ b/web/app/components/workflow/nodes/human-input/panel.tsx @@ -0,0 +1,24 @@ +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import type { HumanInputNodeType } from './types' +import type { NodePanelProps } from '@/app/components/workflow/types' + +const i18nPrefix = 'workflow.nodes.humanInput' + +const Panel: FC> = ({ + id, + data, +}) => { + const { t } = useTranslation() + + return ( +
+
+ TODO +
+
+ ) +} + +export default React.memo(Panel) diff --git a/web/app/components/workflow/nodes/human-input/types.ts b/web/app/components/workflow/nodes/human-input/types.ts new file mode 100644 index 0000000000..e846e3ff85 --- /dev/null +++ b/web/app/components/workflow/nodes/human-input/types.ts @@ -0,0 +1,9 @@ +import type { CommonNodeType, Variable } from '@/app/components/workflow/types' + +export type HumanInputNodeType = CommonNodeType & { + deliveryMethod: any[] + formContent: any + userActions: any[] + timeout: any + outputs: Variable[] +} diff --git a/web/app/components/workflow/nodes/human-input/use-config.ts b/web/app/components/workflow/nodes/human-input/use-config.ts new file mode 100644 index 0000000000..cf17a34bc6 --- /dev/null +++ b/web/app/components/workflow/nodes/human-input/use-config.ts @@ -0,0 +1,27 @@ +import useVarList from '../_base/hooks/use-var-list' +import type { HumanInputNodeType } from './types' +import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' +import { + useNodesReadOnly, +} from '@/app/components/workflow/hooks' +const useConfig = (id: string, payload: HumanInputNodeType) => { + const { nodesReadOnly: readOnly } = useNodesReadOnly() + const { inputs, setInputs } = useNodeCrud(id, payload) + + const { handleVarListChange, handleAddVariable } = useVarList({ + inputs, + setInputs: (newInputs) => { + setInputs(newInputs) + }, + varKey: 'outputs', + }) + + return { + readOnly, + inputs, + handleVarListChange, + handleAddVariable, + } +} + +export default useConfig diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 5f36956798..8667732c45 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -42,6 +42,7 @@ export enum BlockEnum { Loop = 'loop', LoopStart = 'loop-start', LoopEnd = 'loop-end', + HumanInput = 'human-input', } export enum ControlMode { diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 763739ba32..32b8809eed 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -258,6 +258,7 @@ const translation = { 'loop-start': 'Loop Start', 'loop': 'Loop', 'loop-end': 'Exit Loop', + 'human-input': 'Human Input', }, blocksAbout: { 'start': 'Define the initial parameters for launching a workflow', @@ -280,6 +281,7 @@ const translation = { 'document-extractor': 'Used to parse uploaded documents into text content that is easily understandable by LLM.', 'list-operator': 'Used to filter or sort array content.', 'agent': 'Invoking large language models to answer questions or process natural language', + 'human-input': 'Ask for human to confirm before generating the next step', }, operator: { zoomIn: 'Zoom In', @@ -902,6 +904,11 @@ const translation = { clickToViewParameterSchema: 'Click to view parameter schema', parameterSchema: 'Parameter Schema', }, + humanInput: { + deliveryMethod: 'delivery method', + formContent: 'form content', + userActions: 'user actions', + }, }, tracing: { stopBy: 'Stop by {{user}}', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 81e207f67e..bf073c66ed 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -259,6 +259,7 @@ const translation = { 'loop-start': '循环开始', 'loop': '循环', 'loop-end': '退出循环', + 'human-input': '人类输入', }, blocksAbout: { 'start': '定义一个 workflow 流程启动的初始参数', @@ -281,6 +282,7 @@ const translation = { 'document-extractor': '用于将用户上传的文档解析为 LLM 便于理解的文本内容。', 'list-operator': '用于过滤或排序数组内容。', 'agent': '调用大型语言模型回答问题或处理自然语言', + 'human-input': '人工输入,确认后生成下一步', }, operator: { zoomIn: '放大', @@ -903,6 +905,11 @@ const translation = { clickToViewParameterSchema: '点击查看参数 schema', parameterSchema: '参数 Schema', }, + humanInput: { + deliveryMethod: '提交方式', + formContent: '表单内容', + userActions: '用户操作', + }, }, tracing: { stopBy: '由{{user}}终止',