diff --git a/web/src/assets/icon/Icon.tsx b/web/src/assets/icon/Icon.tsx index 1d20e3a84231e82e42138f943fdaf7275e8c3f6d..e363af970b3423f7db084b400ddaa4449521f4ca 100644 --- a/web/src/assets/icon/Icon.tsx +++ b/web/src/assets/icon/Icon.tsx @@ -205,6 +205,36 @@ const QWeatherSvg = () => ( ); +const SemicolonSvg = () => ( + + + +); + +const CommaSvg = () => ( + + + +); + export const ApiIcon = (props: Partial) => ( ); @@ -238,3 +268,11 @@ export const GitHubIcon = (props: Partial) => ( export const QWeatherIcon = (props: Partial) => ( ); + +export const SemicolonIcon = (props: Partial) => ( + +); + +export const CommaIcon = (props: Partial) => ( + +); diff --git a/web/src/components/delimiter.tsx b/web/src/components/delimiter.tsx index 25e3128bde2aa7ed11747d71e6b48eb23234da4d..a79440874d664c1810625120ad09dcb00ba15c94 100644 --- a/web/src/components/delimiter.tsx +++ b/web/src/components/delimiter.tsx @@ -4,16 +4,23 @@ import { useTranslation } from 'react-i18next'; interface IProps { value?: string | undefined; onChange?: (val: string | undefined) => void; + maxLength?: number; } -const DelimiterInput = ({ value, onChange }: IProps) => { +export const DelimiterInput = ({ value, onChange, maxLength }: IProps) => { const nextValue = value?.replaceAll('\n', '\\n'); const handleInputChange = (e: React.ChangeEvent) => { const val = e.target.value; const nextValue = val.replaceAll('\\n', '\n'); onChange?.(nextValue); }; - return ; + return ( + + ); }; const Delimiter = () => { diff --git a/web/src/interfaces/database/flow.ts b/web/src/interfaces/database/flow.ts index f49bda0956045549a584cc2139c3c01e330e9fc3..2a81f92f157db303d72443c2de6915c5190f29a0 100644 --- a/web/src/interfaces/database/flow.ts +++ b/web/src/interfaces/database/flow.ts @@ -17,6 +17,7 @@ export interface IOperator { obj: IOperatorNode; downstream: string[]; upstream: string[]; + parent_id?: string; } export interface IOperatorNode { diff --git a/web/src/less/mixins.less b/web/src/less/mixins.less index 078be91827ae618f2598a0ae2f532ab170c82730..119c1a089a93c4756c842a244ebf7c22e9462ae6 100644 --- a/web/src/less/mixins.less +++ b/web/src/less/mixins.less @@ -75,3 +75,23 @@ background-color: #eff8ff; border: 1px; } + +.commonNodeShadow() { + box-shadow: + -6px 0 12px 0 rgba(179, 177, 177, 0.08), + -3px 0 6px -4px rgba(0, 0, 0, 0.12), + -6px 0 16px 6px rgba(0, 0, 0, 0.05); +} + +.commonNodeRadius() { + border-radius: 10px; +} + +.commonNode() { + .commonNodeShadow(); + .commonNodeRadius(); + + padding: 10px; + background: white; + width: 200px; +} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index f37fe951211f39d5219b38d1e15cc53d6efca51f..d6ccbdcf3e3efb5b67d5a8862534c05d5f404b8e 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1077,6 +1077,22 @@ The above is the content you need to summarize.`, contentTip: 'content: Email content (Optional)', jsonUploadTypeErrorMessage: 'Please upload json file', jsonUploadContentErrorMessage: 'json file error', + iteration: 'Iteration', + iterationDescription: `This component firstly split the input into array by "delimiter". +Perform the same operation steps on the elements in the array in sequence until all results are output, which can be understood as a task batch processor. + +For example, within the long text translation iteration node, if all content is input to the LLM node, the single conversation limit may be reached. The upstream node can first split the long text into multiple fragments, and cooperate with the iterative node to perform batch translation on each fragment to avoid reaching the LLM message limit for a single conversation.`, + delimiterTip: ` +This delimiter is used to split the input text into several text pieces echo of which will be performed as input item of each iteration.`, + delimiterOptions: { + comma: 'Comma', + lineBreak: 'Line break', + tab: 'Tab', + underline: 'Underline', + diagonal: 'Diagonal', + minus: 'Minus', + semicolon: 'Semicolon', + }, }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index d05bf8389cfd3fc98d9962f27036c86b6203bdb2..77af023e06708deb98d476acf86604b0ed3bc9ff 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -1016,6 +1016,20 @@ export default { templateDescription: '此元件用於排版各種元件的輸出。 ', jsonUploadTypeErrorMessage: '請上傳json檔', jsonUploadContentErrorMessage: 'json 檔案錯誤', + iterationDescription: `此元件首先透過「分隔符號」將輸入拆分為陣列。 +對數組中的元素依序執行相同的操作步驟,直到輸出所有結果,可以理解為任務批次處理器。 + +例如,在長文本翻譯迭代節點內,如果所有內容都輸入到LLM節點,則可能會達到單次對話限制。上游節點可以先將長文本拆分為多個分片,並配合迭代節點對每個分片進行批次翻譯,避免達到單次對話的LLM訊息限制。`, + delimiterTip: `此分隔符號用於將輸入文字分割成多個文字片段,其中的回顯將作為每次迭代的輸入項執行。`, + delimiterOptions: { + comma: '逗號', + lineBreak: '換行', + tab: '製表符', + underline: '底線', + diagonal: '斜線', + minus: '減號', + semicolon: '分號', + }, }, footer: { profile: '“保留所有權利 @ react”', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index ca4e46cd4711122ca46afff23a9583b485e1dda8..116654d103fe81947e0825bb914e34ff57ab096b 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1060,6 +1060,20 @@ export default { contentTip: 'content: 邮件内容(可选)', jsonUploadTypeErrorMessage: '请上传json文件', jsonUploadContentErrorMessage: 'json 文件错误', + iteration: '循环', + iterationDescription: `该组件首先将输入以“分隔符”分割成数组,然后依次对数组中的元素执行相同的操作步骤,直到输出所有结果,可以理解为一个任务批处理器。 + +例如在长文本翻译迭代节点中,如果所有内容都输入到LLM节点,可能会达到单次对话的限制,上游节点可以先将长文本分割成多个片段,配合迭代节点对每个片段进行批量翻译,避免达到单次对话的LLM消息限制。`, + delimiterTip: `该分隔符用于将输入文本分割成几个文本片段,每个文本片段的回显将作为每次迭代的输入项。`, + delimiterOptions: { + comma: '逗号', + lineBreak: '换行', + tab: '制表符', + underline: '下划线', + diagonal: '斜线', + minus: '减号', + semicolon: '分号', + }, }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/pages/flow/canvas/edge/index.tsx b/web/src/pages/flow/canvas/edge/index.tsx index 62dc4db973815723b42469efbdb25c461df44288..a1e5fbca3db92af1dc4a98970aa8e0ec39ceff5e 100644 --- a/web/src/pages/flow/canvas/edge/index.tsx +++ b/web/src/pages/flow/canvas/edge/index.tsx @@ -90,6 +90,7 @@ export function ButtonEdge({ // everything inside EdgeLabelRenderer has no pointer events by default // if you have an interactive element, set pointer-events: all pointerEvents: 'all', + zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 }} className="nodrag nopan" > diff --git a/web/src/pages/flow/canvas/index.less b/web/src/pages/flow/canvas/index.less index 3f3245a1c4be0b8b9590425b961252a48296d7f8..d824d88f1bb34d048faf6c61efa74a1d90ca9ad5 100644 --- a/web/src/pages/flow/canvas/index.less +++ b/web/src/pages/flow/canvas/index.less @@ -1,4 +1,10 @@ .canvasWrapper { position: relative; height: 100%; + :global(.react-flow__node-group) { + .commonNode(); + padding: 0; + border: 0; + background-color: transparent; + } } diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx index 4c23a78353c1e095f4da254e7b00a801b704a797..6aa742c3c60f96441ac505678bd18d54b6a59a0d 100644 --- a/web/src/pages/flow/canvas/index.tsx +++ b/web/src/pages/flow/canvas/index.tsx @@ -4,32 +4,24 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; -import { useSetModalState } from '@/hooks/common-hooks'; -import { get } from 'lodash'; import { FolderInput, FolderOutput } from 'lucide-react'; -import { useCallback, useEffect } from 'react'; import ReactFlow, { Background, ConnectionMode, ControlButton, Controls, - NodeMouseHandler, } from 'reactflow'; import 'reactflow/dist/style.css'; import ChatDrawer from '../chat/drawer'; -import { Operator } from '../constant'; import FormDrawer from '../flow-drawer'; import { - useGetBeginNodeDataQuery, useHandleDrop, - useHandleExportOrImportJsonFile, useSelectCanvasData, - useShowFormDrawer, - useShowSingleDebugDrawer, useValidateConnection, useWatchNodeFormDataChange, } from '../hooks'; -import { BeginQuery } from '../interface'; +import { useHandleExportOrImportJsonFile } from '../hooks/use-export-json'; +import { useShowDrawer } from '../hooks/use-show-drawer'; import JsonUploadModal from '../json-upload-modal'; import RunDrawer from '../run-drawer'; import { ButtonEdge } from './edge'; @@ -40,6 +32,7 @@ import { CategorizeNode } from './node/categorize-node'; import { EmailNode } from './node/email-node'; import { GenerateNode } from './node/generate-node'; import { InvokeNode } from './node/invoke-node'; +import { IterationNode, IterationStartNode } from './node/iteration-node'; import { KeywordNode } from './node/keyword-node'; import { LogicNode } from './node/logic-node'; import { MessageNode } from './node/message-node'; @@ -66,6 +59,8 @@ const nodeTypes = { invokeNode: InvokeNode, templateNode: TemplateNode, emailNode: EmailNode, + group: IterationNode, + iterationStartNode: IterationStartNode, }; const edgeTypes = { @@ -87,66 +82,11 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { onSelectionChange, } = useSelectCanvasData(); const isValidConnection = useValidateConnection(); - const { - visible: runVisible, - showModal: showRunModal, - hideModal: hideRunModal, - } = useSetModalState(); - const { - visible: chatVisible, - showModal: showChatModal, - hideModal: hideChatModal, - } = useSetModalState(); - const { - singleDebugDrawerVisible, - showSingleDebugDrawer, - hideSingleDebugDrawer, - } = useShowSingleDebugDrawer(); const controlIconClassname = 'text-black'; - const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } = - useShowFormDrawer(); - - const onPaneClick = useCallback(() => { - hideFormDrawer(); - }, [hideFormDrawer]); - const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(); - useWatchNodeFormDataChange(); - - const hideRunOrChatDrawer = useCallback(() => { - hideChatModal(); - hideRunModal(); - hideDrawer(); - }, [hideChatModal, hideDrawer, hideRunModal]); - - const onNodeClick: NodeMouseHandler = useCallback( - (e, node) => { - if (node.data.label !== Operator.Note) { - hideSingleDebugDrawer(); - hideRunOrChatDrawer(); - showFormDrawer(node); - } - // handle single debug icon click - if ( - get(e.target, 'dataset.play') === 'true' || - get(e.target, 'parentNode.dataset.play') === 'true' - ) { - showSingleDebugDrawer(); - } - }, - [ - hideRunOrChatDrawer, - hideSingleDebugDrawer, - showFormDrawer, - showSingleDebugDrawer, - ], - ); - - const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - const { handleExportJson, handleImportJson, @@ -155,25 +95,25 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { hideFileUploadModal, } = useHandleExportOrImportJsonFile(); - useEffect(() => { - if (drawerVisible) { - const query: BeginQuery[] = getBeginNodeDataQuery(); - if (query.length > 0) { - showRunModal(); - hideChatModal(); - } else { - showChatModal(); - hideRunModal(); - } - } - }, [ - hideChatModal, - hideRunModal, + const { + onNodeClick, + onPaneClick, + clickedNode, + formDrawerVisible, + hideFormDrawer, + singleDebugDrawerVisible, + hideSingleDebugDrawer, + showSingleDebugDrawer, + chatVisible, + runVisible, + hideRunOrChatDrawer, showChatModal, - showRunModal, + } = useShowDrawer({ drawerVisible, - getBeginNodeDataQuery, - ]); + hideDrawer, + }); + + useWatchNodeFormDataChange(); return (
@@ -222,6 +162,7 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { strokeWidth: 2, stroke: 'rgb(202 197 245)', }, + zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498 }} deleteKeyCode={['Delete', 'Backspace']} > diff --git a/web/src/pages/flow/canvas/node/begin-node.tsx b/web/src/pages/flow/canvas/node/begin-node.tsx index 9f0ba05e224d247e89b47dd86c6a872db2a347b6..35a9560e4c9310c065d2f28f39e6717aa74f0aba 100644 --- a/web/src/pages/flow/canvas/node/begin-node.tsx +++ b/web/src/pages/flow/canvas/node/begin-node.tsx @@ -44,7 +44,9 @@ export function BeginNode({ selected, data }: NodeProps) { fontSize={24} color={operatorMap[data.label as Operator].color} > -
{t(`flow.begin`)}
+
+ {t(`flow.begin`)} +
{query.map((x, idx) => { diff --git a/web/src/pages/flow/canvas/node/dropdown.tsx b/web/src/pages/flow/canvas/node/dropdown.tsx index 7e6fb1e98fab6e2a4b86f350ce5140776c2fd1b1..dd5263abc60ba88fbe19b157d9da6fcd8f325dc3 100644 --- a/web/src/pages/flow/canvas/node/dropdown.tsx +++ b/web/src/pages/flow/canvas/node/dropdown.tsx @@ -3,6 +3,7 @@ import { CopyOutlined } from '@ant-design/icons'; import { Flex, MenuProps } from 'antd'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { Operator } from '../../constant'; import { useDuplicateNode } from '../../hooks'; import useGraphStore from '../../store'; @@ -15,10 +16,17 @@ interface IProps { const NodeDropdown = ({ id, iconFontColor, label }: IProps) => { const { t } = useTranslation(); const deleteNodeById = useGraphStore((store) => store.deleteNodeById); + const deleteIterationNodeById = useGraphStore( + (store) => store.deleteIterationNodeById, + ); const deleteNode = useCallback(() => { - deleteNodeById(id); - }, [id, deleteNodeById]); + if (label === Operator.Iteration) { + deleteIterationNodeById(id); + } else { + deleteNodeById(id); + } + }, [label, deleteIterationNodeById, id, deleteNodeById]); const duplicateNode = useDuplicateNode(); diff --git a/web/src/pages/flow/canvas/node/generate-node.tsx b/web/src/pages/flow/canvas/node/generate-node.tsx index 4d39821eb42badbbf5cf925ba27a1fd582a13688..d79d1dc6cc44d3aba80a8e27f41898d8c6ca7479 100644 --- a/web/src/pages/flow/canvas/node/generate-node.tsx +++ b/web/src/pages/flow/canvas/node/generate-node.tsx @@ -4,7 +4,7 @@ import { Flex } from 'antd'; import classNames from 'classnames'; import { get } from 'lodash'; import { Handle, NodeProps, Position } from 'reactflow'; -import { useGetComponentLabelByValue } from '../../hooks'; +import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; import { IGenerateParameter, NodeData } from '../../interface'; import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import styles from './index.less'; diff --git a/web/src/pages/flow/canvas/node/index.less b/web/src/pages/flow/canvas/node/index.less index e6eb6cc4e1fc9b32b22f20bf4d20a4f1f57d013a..14d7e60779cc2ca1dd935d5e6d5b0d7aeb095d6f 100644 --- a/web/src/pages/flow/canvas/node/index.less +++ b/web/src/pages/flow/canvas/node/index.less @@ -1,15 +1,3 @@ -.commonNode() { - box-shadow: - -6px 0 12px 0 rgba(179, 177, 177, 0.08), - -3px 0 6px -4px rgba(0, 0, 0, 0.12), - -6px 0 16px 6px rgba(0, 0, 0, 0.05); - - padding: 10px; - border-radius: 10px; - background: white; - width: 200px; -} - .dark { background: rgb(63, 63, 63) !important; } @@ -43,6 +31,22 @@ border: 1.5px solid rgb(59, 118, 244); } +.selectedIterationNode { + border-bottom: 1.5px solid rgb(59, 118, 244); + border-left: 1.5px solid rgb(59, 118, 244); + border-right: 1.5px solid rgb(59, 118, 244); +} + +.iterationHeader { + .commonNodeShadow(); +} + +.selectedHeader { + border-top: 1.9px solid rgb(59, 118, 244); + border-left: 1.9px solid rgb(59, 118, 244); + border-right: 1.9px solid rgb(59, 118, 244); +} + .handle { display: inline-flex; align-items: center; @@ -133,6 +137,12 @@ } } +.iterationNode { + .commonNodeShadow(); + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; +} + .nodeText { padding-inline: 0.4em; padding-block: 0.2em 0.1em; @@ -142,12 +152,6 @@ .textEllipsis(); } -.nodeTitle { - font-weight: 600; - text-align: center; - .textEllipsis(); -} - .nodeHeader { padding-bottom: 12px; } diff --git a/web/src/pages/flow/canvas/node/iteration-node.tsx b/web/src/pages/flow/canvas/node/iteration-node.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4a81d79424e945002fdebe7fc7ddce6a806b0659 --- /dev/null +++ b/web/src/pages/flow/canvas/node/iteration-node.tsx @@ -0,0 +1,118 @@ +import { useTheme } from '@/components/theme-provider'; +import { cn } from '@/lib/utils'; +import { ListRestart } from 'lucide-react'; +import { Handle, NodeProps, NodeResizeControl, Position } from 'reactflow'; +import { NodeData } from '../../interface'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; + +function ResizeIcon() { + return ( + + + + + + + + ); +} + +const controlStyle = { + background: 'transparent', + border: 'none', +}; + +export function IterationNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const { theme } = useTheme(); + + return ( +
+ + + + + + +
+ ); +} + +export function IterationStartNode({ + isConnectable = true, + selected, +}: NodeProps) { + const { theme } = useTheme(); + + return ( +
+ +
+ +
+
+ ); +} diff --git a/web/src/pages/flow/canvas/node/node-header.tsx b/web/src/pages/flow/canvas/node/node-header.tsx index f9bb2870603ce0ab2efaf5a63410d1c504e7f871..4c7ad21f2827f3ee1122a53c7672795f9299b3d5 100644 --- a/web/src/pages/flow/canvas/node/node-header.tsx +++ b/web/src/pages/flow/canvas/node/node-header.tsx @@ -8,15 +8,17 @@ import NodeDropdown from './dropdown'; import { NextNodePopover } from './popover'; import { RunTooltip } from '../../flow-tooltip'; -import styles from './index.less'; interface IProps { id: string; label: string; name: string; gap?: number; className?: string; + wrapperClassName?: string; } +const ExcludedRunStateOperators = [Operator.Answer]; + export function RunStatus({ id, name, label }: IProps) { const { t } = useTranslate('flow'); return ( @@ -35,10 +37,17 @@ export function RunStatus({ id, name, label }: IProps) { ); } -const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => { +const NodeHeader = ({ + label, + id, + name, + gap = 4, + className, + wrapperClassName, +}: IProps) => { return ( -
- {label !== Operator.Answer && ( +
+ {!ExcludedRunStateOperators.includes(label as Operator) && ( )} { name={label as Operator} color={operatorMap[label as Operator].color} > - {name} + + {name} +
diff --git a/web/src/pages/flow/canvas/node/popover.tsx b/web/src/pages/flow/canvas/node/popover.tsx index 042c6f30cfd16c6321c1fa1caa449c4d91dc5fdf..342ce40ebabdd149e2d6e7c4d4c679928c7d6357 100644 --- a/web/src/pages/flow/canvas/node/popover.tsx +++ b/web/src/pages/flow/canvas/node/popover.tsx @@ -3,7 +3,7 @@ import get from 'lodash/get'; import React, { MouseEventHandler, useCallback, useMemo } from 'react'; import JsonView from 'react18-json-view'; import 'react18-json-view/src/style.css'; -import { useGetComponentLabelByValue, useReplaceIdWithText } from '../../hooks'; +import { useReplaceIdWithText } from '../../hooks'; import { useTheme } from '@/components/theme-provider'; import { @@ -20,6 +20,7 @@ import { TableRow, } from '@/components/ui/table'; import { useTranslate } from '@/hooks/common-hooks'; +import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; interface IProps extends React.PropsWithChildren { nodeId: string; diff --git a/web/src/pages/flow/canvas/node/switch-node.tsx b/web/src/pages/flow/canvas/node/switch-node.tsx index 5a1bd5142b965a267ef889ac90edd57a983cc38d..e55b7ac2ec15a05b9206fd4397a695ede533a60b 100644 --- a/web/src/pages/flow/canvas/node/switch-node.tsx +++ b/web/src/pages/flow/canvas/node/switch-node.tsx @@ -2,7 +2,7 @@ import { useTheme } from '@/components/theme-provider'; import { Divider, Flex } from 'antd'; import classNames from 'classnames'; import { Handle, NodeProps, Position } from 'reactflow'; -import { useGetComponentLabelByValue } from '../../hooks'; +import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; import { ISwitchCondition, NodeData } from '../../interface'; import { RightHandleStyle } from './handle-icon'; import { useBuildSwitchHandlePositions } from './hooks'; diff --git a/web/src/pages/flow/canvas/node/template-node.tsx b/web/src/pages/flow/canvas/node/template-node.tsx index 8a6d1ff614e08dda826b924ae79179086632cfe2..c16286df52c35b7e73dadd3fe98c9e7334f57fa0 100644 --- a/web/src/pages/flow/canvas/node/template-node.tsx +++ b/web/src/pages/flow/canvas/node/template-node.tsx @@ -1,13 +1,13 @@ +import { useTheme } from '@/components/theme-provider'; import { Flex } from 'antd'; import classNames from 'classnames'; import { get } from 'lodash'; import { Handle, NodeProps, Position } from 'reactflow'; -import { useGetComponentLabelByValue } from '../../hooks'; +import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; import { IGenerateParameter, NodeData } from '../../interface'; import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import NodeHeader from './node-header'; -import { useTheme } from '@/components/theme-provider'; import styles from './index.less'; export function TemplateNode({ diff --git a/web/src/pages/flow/constant.tsx b/web/src/pages/flow/constant.tsx index 6ea265f2a8fde25d00c27a06c06e2e32484a9409..9b3d76cc47ed8f67460b3d8a2b06b847a31f1090 100644 --- a/web/src/pages/flow/constant.tsx +++ b/web/src/pages/flow/constant.tsx @@ -50,7 +50,9 @@ import { } from '@ant-design/icons'; import upperFirst from 'lodash/upperFirst'; import { + CirclePower, CloudUpload, + IterationCcw, ListOrdered, OptionIcon, TextCursorInput, @@ -58,6 +60,8 @@ import { WrapText, } from 'lucide-react'; +export const BeginId = 'begin'; + export enum Operator { Begin = 'Begin', Retrieval = 'Retrieval', @@ -93,6 +97,8 @@ export enum Operator { Invoke = 'Invoke', Template = 'Template', Email = 'Email', + Iteration = 'Iteration', + IterationStart = 'IterationItem', } export const CommonOperatorList = Object.values(Operator).filter( @@ -134,6 +140,8 @@ export const operatorIconMap = { [Operator.Invoke]: InvokeIcon, [Operator.Template]: TemplateIcon, [Operator.Email]: EmailIcon, + [Operator.Iteration]: IterationCcw, + [Operator.IterationStart]: CirclePower, }; export const operatorMap: Record< @@ -270,6 +278,8 @@ export const operatorMap: Record< backgroundColor: '#dee0e2', }, [Operator.Email]: { backgroundColor: '#e6f7ff' }, + [Operator.Iteration]: { backgroundColor: '#e6f7ff' }, + [Operator.IterationStart]: { backgroundColor: '#e6f7ff' }, }; export const componentMenuList = [ @@ -306,6 +316,9 @@ export const componentMenuList = [ { name: Operator.Template, }, + { + name: Operator.Iteration, + }, { name: Operator.Note, }, @@ -606,6 +619,11 @@ export const initialEmailValues = { content: '', }; +export const initialIterationValues = { + delimiter: ',', +}; +export const initialIterationStartValues = {}; + export const CategorizeAnchorPointPositions = [ { top: 1, right: 34 }, { top: 8, right: 18 }, @@ -687,6 +705,8 @@ export const RestrictedUpstreamMap = { [Operator.Invoke]: [Operator.Begin], [Operator.Template]: [Operator.Begin, Operator.Relevant], [Operator.Email]: [Operator.Begin], + [Operator.Iteration]: [Operator.Begin], + [Operator.IterationStart]: [Operator.Begin], }; export const NodeMap = { @@ -724,6 +744,8 @@ export const NodeMap = { [Operator.Invoke]: 'invokeNode', [Operator.Template]: 'templateNode', [Operator.Email]: 'emailNode', + [Operator.Iteration]: 'group', + [Operator.IterationStart]: 'iterationStartNode', }; export const LanguageOptions = [ @@ -2940,4 +2962,5 @@ export const NoDebugOperatorsList = [ Operator.Message, Operator.RewriteQuestion, Operator.Switch, + Operator.Iteration, ]; diff --git a/web/src/pages/flow/flow-drawer/index.tsx b/web/src/pages/flow/flow-drawer/index.tsx index 31ef22ac7ae37a3a3c8d2125de04448b3fddfc29..3014d9dfc77a35c0ebc371c2cb569a77e765f3d6 100644 --- a/web/src/pages/flow/flow-drawer/index.tsx +++ b/web/src/pages/flow/flow-drawer/index.tsx @@ -6,7 +6,7 @@ import { lowerFirst } from 'lodash'; import { Play } from 'lucide-react'; import { useEffect, useRef } from 'react'; import { Node } from 'reactflow'; -import { Operator, operatorMap } from '../constant'; +import { BeginId, Operator, operatorMap } from '../constant'; import AkShareForm from '../form/akshare-form'; import AnswerForm from '../form/answer-form'; import ArXivForm from '../form/arxiv-form'; @@ -45,6 +45,7 @@ import { getDrawerWidth, needsSingleStepDebugging } from '../utils'; import SingleDebugDrawer from './single-debug-drawer'; import { RunTooltip } from '../flow-tooltip'; +import IterationForm from '../form/iteration-from'; import styles from './index.less'; interface IProps { @@ -89,6 +90,8 @@ const FormMap = { [Operator.Note]: () => <>, [Operator.Template]: TemplateForm, [Operator.Email]: EmailForm, + [Operator.Iteration]: IterationForm, + [Operator.IterationStart]: () => <>, }; const EmptyContent = () =>
; @@ -137,11 +140,15 @@ const FormDrawer = ({ - + {node?.id === BeginId ? ( + {t(BeginId)} + ) : ( + + )} {needsSingleStepDebugging(operatorName) && ( diff --git a/web/src/pages/flow/form/akshare-form/index.tsx b/web/src/pages/flow/form/akshare-form/index.tsx index 1e38fd930e94f413750512f9e67696796c18b452..1f7ce99f1c350cbe32e3dd4404554c49d78028e8 100644 --- a/web/src/pages/flow/form/akshare-form/index.tsx +++ b/web/src/pages/flow/form/akshare-form/index.tsx @@ -12,7 +12,7 @@ const AkShareForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + ); diff --git a/web/src/pages/flow/form/arxiv-form/index.tsx b/web/src/pages/flow/form/arxiv-form/index.tsx index acae1b85e6edfa3dd10c284a8016372e06450dd4..a445921483435f10ceec4d205e44dbd5515dd3ad 100644 --- a/web/src/pages/flow/form/arxiv-form/index.tsx +++ b/web/src/pages/flow/form/arxiv-form/index.tsx @@ -23,7 +23,7 @@ const ArXivForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/baidu-fanyi-form/index.tsx b/web/src/pages/flow/form/baidu-fanyi-form/index.tsx index 37cd376c0394eec53610748543929542f22e4d18..c4b3990269ac3ee3615f51ea3a8a5ea2ddfdb1c4 100644 --- a/web/src/pages/flow/form/baidu-fanyi-form/index.tsx +++ b/web/src/pages/flow/form/baidu-fanyi-form/index.tsx @@ -39,7 +39,7 @@ const BaiduFanyiForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/baidu-form/index.tsx b/web/src/pages/flow/form/baidu-form/index.tsx index bc810c0618a9701718e3b0422a898e1bd11e9b15..0c866e488500be368f1ca63a3434d0c85a3ada6c 100644 --- a/web/src/pages/flow/form/baidu-form/index.tsx +++ b/web/src/pages/flow/form/baidu-form/index.tsx @@ -12,7 +12,7 @@ const BaiduForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + ); diff --git a/web/src/pages/flow/form/bing-form/index.tsx b/web/src/pages/flow/form/bing-form/index.tsx index 02645313802167cb0933f5788ecdfa9bcec48aee..b640f08d0ea9960ed49365c6e3ba82c011b918c6 100644 --- a/web/src/pages/flow/form/bing-form/index.tsx +++ b/web/src/pages/flow/form/bing-form/index.tsx @@ -21,7 +21,7 @@ const BingForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/categorize-form/index.tsx b/web/src/pages/flow/form/categorize-form/index.tsx index 00debf47f12820a74ca68a1d3f49f66d3d2f2a69..cb6651a24879c82113c003aa441f12a49b600b6b 100644 --- a/web/src/pages/flow/form/categorize-form/index.tsx +++ b/web/src/pages/flow/form/categorize-form/index.tsx @@ -24,7 +24,7 @@ const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => { initialValues={{ items: [{}] }} layout={'vertical'} > - + ; } enum VariableType { @@ -18,9 +20,12 @@ enum VariableType { const getVariableName = (type: string) => type === VariableType.Reference ? 'component_id' : 'value'; -const DynamicVariableForm = ({ nodeId }: IProps) => { +const DynamicVariableForm = ({ node }: IProps) => { const { t } = useTranslation(); - const valueOptions = useBuildComponentIdSelectOptions(nodeId); + const valueOptions = useBuildComponentIdSelectOptions( + node?.id, + node?.parentId, + ); const form = Form.useFormInstance(); const options = [ @@ -114,11 +119,11 @@ export function FormCollapse({ ); } -const DynamicInputVariable = ({ nodeId }: IProps) => { +const DynamicInputVariable = ({ node }: IProps) => { const { t } = useTranslation(); return ( - + ); }; diff --git a/web/src/pages/flow/form/crawler-form/index.tsx b/web/src/pages/flow/form/crawler-form/index.tsx index 52e84567bd4c4ab686251e59f986b8cd610b0b95..8ef5f14d6b4d39d02f7f5ffc3112a558a7ae7ebd 100644 --- a/web/src/pages/flow/form/crawler-form/index.tsx +++ b/web/src/pages/flow/form/crawler-form/index.tsx @@ -20,7 +20,7 @@ const CrawlerForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/deepl-form/index.tsx b/web/src/pages/flow/form/deepl-form/index.tsx index f532430a69e6ae6f695d4dfeda2de1b8d325ebc9..1fc8cfc26f37111b9612c14478d462dbe268748a 100644 --- a/web/src/pages/flow/form/deepl-form/index.tsx +++ b/web/src/pages/flow/form/deepl-form/index.tsx @@ -18,7 +18,7 @@ const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/duckduckgo-form/index.tsx b/web/src/pages/flow/form/duckduckgo-form/index.tsx index 68b611415ec92a436f8cd12bff5f1f62f54a72cf..53462da31ecc57e0b3b480b1d48508dfb7b50ca4 100644 --- a/web/src/pages/flow/form/duckduckgo-form/index.tsx +++ b/web/src/pages/flow/form/duckduckgo-form/index.tsx @@ -21,7 +21,7 @@ const DuckDuckGoForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + { onValuesChange={onValuesChange} layout={'vertical'} > - + {/* SMTP服务器配置 */} diff --git a/web/src/pages/flow/form/exesql-form/index.tsx b/web/src/pages/flow/form/exesql-form/index.tsx index d8541e532965123f9df71c4ab8cd66ef4f76b372..010d5518ed342d7bf29444a2749c388d200c936b 100644 --- a/web/src/pages/flow/form/exesql-form/index.tsx +++ b/web/src/pages/flow/form/exesql-form/index.tsx @@ -24,7 +24,7 @@ const ExeSQLForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + ; } const components = { @@ -19,10 +19,11 @@ const components = { }, }; -const DynamicParameters = ({ nodeId }: IProps) => { +const DynamicParameters = ({ node }: IProps) => { + const nodeId = node?.id; const { t } = useTranslate('flow'); - const options = useBuildComponentIdSelectOptions(nodeId); + const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId); const { dataSource, handleAdd, diff --git a/web/src/pages/flow/form/generate-form/index.tsx b/web/src/pages/flow/form/generate-form/index.tsx index 0b5a77b1a594d0523a800c0868f4f833f92999ff..500d42fbd0edd8fb91677f6f20d0ff9c1df865e5 100644 --- a/web/src/pages/flow/form/generate-form/index.tsx +++ b/web/src/pages/flow/form/generate-form/index.tsx @@ -49,7 +49,7 @@ const GenerateForm = ({ onValuesChange, form, node }: IOperatorForm) => { - + ); }; diff --git a/web/src/pages/flow/form/github-form/index.tsx b/web/src/pages/flow/form/github-form/index.tsx index e81c79fa808b47150b682aff5a6d188a36394ec2..691dd5d36369e0b7034e5fe3dca3cd695c012802 100644 --- a/web/src/pages/flow/form/github-form/index.tsx +++ b/web/src/pages/flow/form/github-form/index.tsx @@ -12,7 +12,7 @@ const GithubForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + ); diff --git a/web/src/pages/flow/form/google-form/index.tsx b/web/src/pages/flow/form/google-form/index.tsx index 310cde2876a3b7c4072031dac4869213a16c8c13..75bd3fb5e5074a3886927e005f2e73aa738e3bc4 100644 --- a/web/src/pages/flow/form/google-form/index.tsx +++ b/web/src/pages/flow/form/google-form/index.tsx @@ -16,7 +16,7 @@ const GoogleForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/google-scholar-form/index.tsx b/web/src/pages/flow/form/google-scholar-form/index.tsx index ce320b309588078bf472fc65608bfc64eeffb543..4e87fac2561d3b092d676c335f186e1c3cac0ff3 100644 --- a/web/src/pages/flow/form/google-scholar-form/index.tsx +++ b/web/src/pages/flow/form/google-scholar-form/index.tsx @@ -45,7 +45,7 @@ const GoogleScholarForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + ; } const components = { @@ -20,10 +21,11 @@ const components = { }, }; -const DynamicVariablesForm = ({ nodeId }: IProps) => { +const DynamicVariablesForm = ({ node }: IProps) => { + const nodeId = node?.id; const { t } = useTranslate('flow'); - const options = useBuildComponentIdSelectOptions(nodeId); + const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId); const { dataSource, handleAdd, diff --git a/web/src/pages/flow/form/invoke-form/index.tsx b/web/src/pages/flow/form/invoke-form/index.tsx index f3f2db5e3ea09350eb5655f1ceb98e6ade025841..521f1c95f6cd4aa77d69890df96ea1d733b7e53a 100644 --- a/web/src/pages/flow/form/invoke-form/index.tsx +++ b/web/src/pages/flow/form/invoke-form/index.tsx @@ -69,7 +69,7 @@ const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => { > - + ); diff --git a/web/src/pages/flow/form/iteration-from/index.tsx b/web/src/pages/flow/form/iteration-from/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f0f23918a9f0c0c614809c8f67647205475cd470 --- /dev/null +++ b/web/src/pages/flow/form/iteration-from/index.tsx @@ -0,0 +1,94 @@ +import { CommaIcon, SemicolonIcon } from '@/assets/icon/Icon'; +import { Form, Select } from 'antd'; +import { + CornerDownLeft, + IndentIncrease, + Minus, + Slash, + Underline, +} from 'lucide-react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IOperatorForm } from '../../interface'; +import DynamicInputVariable from '../components/dynamic-input-variable'; + +const optionList = [ + { + value: ',', + icon: CommaIcon, + text: 'comma', + }, + { + value: '\n', + icon: CornerDownLeft, + text: 'lineBreak', + }, + { + value: 'tab', + icon: IndentIncrease, + text: 'tab', + }, + { + value: '_', + icon: Underline, + text: 'underline', + }, + { + value: '/', + icon: Slash, + text: 'diagonal', + }, + { + value: '-', + icon: Minus, + text: 'minus', + }, + { + value: ';', + icon: SemicolonIcon, + text: 'semicolon', + }, +]; + +const IterationForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslation(); + + const options = useMemo(() => { + return optionList.map((x) => { + let Icon = x.icon; + + return { + value: x.value, + label: ( +
+ + {t(`flow.delimiterOptions.${x.text}`)} +
+ ), + }; + }); + }, [t]); + + return ( +
+ + + + +
+ ); +}; + +export default IterationForm; diff --git a/web/src/pages/flow/form/jin10-form/index.tsx b/web/src/pages/flow/form/jin10-form/index.tsx index 94ac5f1745e66791b61a9bf30095179215ed7aad..aa9bb169fbfd7d3a88b2722a24cf1d300e12f92f 100644 --- a/web/src/pages/flow/form/jin10-form/index.tsx +++ b/web/src/pages/flow/form/jin10-form/index.tsx @@ -65,7 +65,7 @@ const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/keyword-extract-form/index.tsx b/web/src/pages/flow/form/keyword-extract-form/index.tsx index fa969724b97f30d9d2388be04a7d56bf7f6c0273..c4c448c7a22fe9e1dd3442ea1c0db61e32f8339c 100644 --- a/web/src/pages/flow/form/keyword-extract-form/index.tsx +++ b/web/src/pages/flow/form/keyword-extract-form/index.tsx @@ -16,7 +16,7 @@ const KeywordExtractForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + { onValuesChange={onValuesChange} layout={'vertical'} > - + { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/retrieval-form/index.tsx b/web/src/pages/flow/form/retrieval-form/index.tsx index df4b7083f4c0b0ef3fc609507a9944c167b311ec..4a92a7f94fc605a180d36074db6bb8366857a07d 100644 --- a/web/src/pages/flow/form/retrieval-form/index.tsx +++ b/web/src/pages/flow/form/retrieval-form/index.tsx @@ -32,7 +32,7 @@ const RetrievalForm = ({ onValuesChange, form, node }: IOperatorForm) => { form={form} layout={'vertical'} > - + { })); }, [t]); - const componentIdOptions = useBuildComponentIdSelectOptions(node?.id); + const componentIdOptions = useBuildComponentIdSelectOptions( + node?.id, + node?.parentId, + ); return (
{ - + ); }; diff --git a/web/src/pages/flow/form/tushare-form/index.tsx b/web/src/pages/flow/form/tushare-form/index.tsx index fea408df90e14944e63951592466fcf501ea3a61..01b11c220d3ae3aa15f4e92545510de6812a3972 100644 --- a/web/src/pages/flow/form/tushare-form/index.tsx +++ b/web/src/pages/flow/form/tushare-form/index.tsx @@ -56,7 +56,7 @@ const TuShareForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/wikipedia-form/index.tsx b/web/src/pages/flow/form/wikipedia-form/index.tsx index df5595a8ef9175a87e9a3cca5f85d49d35e17c7e..9e28bf21d169a1aa6657065b64ac50aed4832799 100644 --- a/web/src/pages/flow/form/wikipedia-form/index.tsx +++ b/web/src/pages/flow/form/wikipedia-form/index.tsx @@ -16,7 +16,7 @@ const WikipediaForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/form/yahoo-finance-form/index.tsx b/web/src/pages/flow/form/yahoo-finance-form/index.tsx index 44598298cd044d4db01cf2211ffa1cab2064d1d4..ce7a3e7d2e5698d870a98c0df81a32830b39d3d5 100644 --- a/web/src/pages/flow/form/yahoo-finance-form/index.tsx +++ b/web/src/pages/flow/form/yahoo-finance-form/index.tsx @@ -14,7 +14,7 @@ const YahooFinanceForm = ({ onValuesChange, form, node }: IOperatorForm) => { onValuesChange={onValuesChange} layout={'vertical'} > - + diff --git a/web/src/pages/flow/header/index.tsx b/web/src/pages/flow/header/index.tsx index a517727bac7d4e76e2843deb7b3a70470ad99319..245e33101efb4db071d6db0cf343fc80f7fc9908 100644 --- a/web/src/pages/flow/header/index.tsx +++ b/web/src/pages/flow/header/index.tsx @@ -10,11 +10,14 @@ import { Link, useParams } from 'umi'; import { useGetBeginNodeDataQuery, useGetBeginNodeDataQueryIsEmpty, +} from '../hooks/use-get-begin-query'; +import { useSaveGraph, useSaveGraphBeforeOpeningDebugDrawer, useWatchAgentChange, -} from '../hooks'; +} from '../hooks/use-save-graph'; import { BeginQuery } from '../interface'; + import styles from './index.less'; interface IProps { diff --git a/web/src/pages/flow/hooks.tsx b/web/src/pages/flow/hooks.tsx index 9608e3b13475f5daf0cb9187eb25c24d6b5c85fb..aff2ef38d01616aa44c806cc69a3dd098533726f 100644 --- a/web/src/pages/flow/hooks.tsx +++ b/web/src/pages/flow/hooks.tsx @@ -1,7 +1,3 @@ -import { useSetModalState } from '@/hooks/common-hooks'; -import { useFetchFlow, useResetFlow, useSetFlow } from '@/hooks/flow-hooks'; -import { IGraph } from '@/interfaces/database/flow'; -import { useIsFetching } from '@tanstack/react-query'; import React, { ChangeEvent, useCallback, @@ -12,23 +8,17 @@ import React, { import { Connection, Edge, Node, Position, ReactFlowInstance } from 'reactflow'; // import { shallow } from 'zustand/shallow'; import { variableEnabledFieldMap } from '@/constants/chat'; -import { FileMimeType } from '@/constants/common'; import { ModelVariableType, settledModelVariableMap, } from '@/constants/knowledge'; import { useFetchModelId } from '@/hooks/logic-hooks'; import { Variable } from '@/interfaces/database/chat'; -import { downloadJsonFile } from '@/utils/file-util'; -import { useDebounceEffect } from 'ahooks'; -import { FormInstance, UploadFile, message } from 'antd'; -import { DefaultOptionType } from 'antd/es/select'; -import dayjs from 'dayjs'; +import { FormInstance, message } from 'antd'; import { humanId } from 'human-id'; import { get, isEmpty, lowerFirst, pick } from 'lodash'; import trim from 'lodash/trim'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'umi'; import { v4 as uuid } from 'uuid'; import { NodeMap, @@ -53,6 +43,7 @@ import { initialGoogleScholarValues, initialGoogleValues, initialInvokeValues, + initialIterationValues, initialJin10Values, initialKeywordExtractValues, initialMessageValues, @@ -69,18 +60,13 @@ import { initialWikipediaValues, initialYahooFinanceValues, } from './constant'; -import { - BeginQuery, - ICategorizeForm, - IRelevantForm, - ISwitchForm, -} from './interface'; +import { ICategorizeForm, IRelevantForm, ISwitchForm } from './interface'; import useGraphStore, { RFState } from './store'; import { - buildDslComponentsByGraph, generateNodeNamesWithIncreasingIndex, generateSwitchHandleText, getNodeDragHandle, + getRelativePositionToIterationNode, replaceIdWithText, } from './utils'; @@ -145,6 +131,8 @@ export const useInitializeOperatorParams = () => { [Operator.Invoke]: initialInvokeValues, [Operator.Template]: initialTemplateValues, [Operator.Email]: initialEmailValues, + [Operator.Iteration]: initialIterationValues, + [Operator.IterationStart]: initialIterationValues, }; }, [llmId]); @@ -210,7 +198,7 @@ export const useHandleDrop = () => { x: event.clientX, y: event.clientY, }); - const newNode = { + const newNode: Node = { id: `${type}:${humanId()}`, type: NodeMap[type as Operator] || 'ragNode', position: position || { @@ -227,7 +215,38 @@ export const useHandleDrop = () => { dragHandle: getNodeDragHandle(type), }; - addNode(newNode); + if (type === Operator.Iteration) { + newNode.style = { + width: 500, + height: 250, + }; + const iterationStartNode: Node = { + id: `${Operator.IterationStart}:${humanId()}`, + type: 'iterationStartNode', + position: { x: 50, y: 100 }, + // draggable: false, + data: { + label: Operator.IterationStart, + name: Operator.IterationStart, + form: {}, + }, + parentId: newNode.id, + extent: 'parent', + }; + addNode(newNode); + addNode(iterationStartNode); + } else { + const subNodeOfIteration = getRelativePositionToIterationNode( + nodes, + position, + ); + if (subNodeOfIteration) { + newNode.parentId = subNodeOfIteration.parentId; + newNode.position = subNodeOfIteration.position; + newNode.extent = 'parent'; + } + addNode(newNode); + } }, [reactFlowInstance, getNodeName, nodes, initializeOperatorParams, addNode], ); @@ -235,78 +254,6 @@ export const useHandleDrop = () => { return { onDrop, onDragOver, setReactFlowInstance }; }; -export const useShowFormDrawer = () => { - const { - clickedNodeId: clickNodeId, - setClickedNodeId, - getNode, - } = useGraphStore((state) => state); - const { - visible: formDrawerVisible, - hideModal: hideFormDrawer, - showModal: showFormDrawer, - } = useSetModalState(); - - const handleShow = useCallback( - (node: Node) => { - setClickedNodeId(node.id); - showFormDrawer(); - }, - [showFormDrawer, setClickedNodeId], - ); - - return { - formDrawerVisible, - hideFormDrawer, - showFormDrawer: handleShow, - clickedNode: getNode(clickNodeId), - }; -}; - -export const useBuildDslData = () => { - const { data } = useFetchFlow(); - const { nodes, edges } = useGraphStore((state) => state); - - const buildDslData = useCallback( - (currentNodes?: Node[]) => { - const dslComponents = buildDslComponentsByGraph( - currentNodes ?? nodes, - edges, - data.dsl.components, - ); - - return { - ...data.dsl, - graph: { nodes: currentNodes ?? nodes, edges }, - components: dslComponents, - }; - }, - [data.dsl, edges, nodes], - ); - - return { buildDslData }; -}; - -export const useSaveGraph = () => { - const { data } = useFetchFlow(); - const { setFlow, loading } = useSetFlow(); - const { id } = useParams(); - const { buildDslData } = useBuildDslData(); - - const saveGraph = useCallback( - async (currentNodes?: Node[]) => { - return setFlow({ - id, - title: data.title, - dsl: buildDslData(currentNodes), - }); - }, - [setFlow, id, data.title, buildDslData], - ); - - return { saveGraph, loading }; -}; - export const useHandleFormValuesChange = (id?: string) => { const updateNodeForm = useGraphStore((state) => state.updateNodeForm); const handleValuesChange = useCallback( @@ -335,39 +282,6 @@ export const useHandleFormValuesChange = (id?: string) => { return { handleValuesChange }; }; -const useSetGraphInfo = () => { - const { setEdges, setNodes } = useGraphStore((state) => state); - const setGraphInfo = useCallback( - ({ nodes = [], edges = [] }: IGraph) => { - if (nodes.length || edges.length) { - setNodes(nodes); - setEdges(edges); - } - }, - [setEdges, setNodes], - ); - return setGraphInfo; -}; - -export const useFetchDataOnMount = () => { - const { loading, data, refetch } = useFetchFlow(); - const setGraphInfo = useSetGraphInfo(); - - useEffect(() => { - setGraphInfo(data?.dsl?.graph ?? ({} as IGraph)); - }, [setGraphInfo, data]); - - useEffect(() => { - refetch(); - }, [refetch]); - - return { loading, flowDetail: data }; -}; - -export const useFlowIsFetching = () => { - return useIsFetching({ queryKey: ['flowDetail'] }) > 0; -}; - export const useSetLlmSetting = ( form?: FormInstance, formData?: Record, @@ -401,7 +315,22 @@ export const useSetLlmSetting = ( }; export const useValidateConnection = () => { - const { edges, getOperatorTypeFromId } = useGraphStore((state) => state); + const { edges, getOperatorTypeFromId, getParentIdById } = useGraphStore( + (state) => state, + ); + + const isSameNodeChild = useCallback( + (connection: Connection) => { + const sourceParentId = getParentIdById(connection.source); + const targetParentId = getParentIdById(connection.target); + if (sourceParentId || targetParentId) { + return sourceParentId === targetParentId; + } + return true; + }, + [getParentIdById], + ); + // restricted lines cannot be connected successfully. const isValidConnection = useCallback( (connection: Connection) => { @@ -418,10 +347,11 @@ export const useValidateConnection = () => { !hasLine && RestrictedUpstreamMap[ getOperatorTypeFromId(connection.source) as Operator - ]?.every((x) => x !== getOperatorTypeFromId(connection.target)); + ]?.every((x) => x !== getOperatorTypeFromId(connection.target)) && + isSameNodeChild(connection); return ret; }, - [edges, getOperatorTypeFromId], + [edges, getOperatorTypeFromId, isSameNodeChild], ); return isValidConnection; @@ -464,52 +394,6 @@ export const useHandleNodeNameChange = ({ return { name, handleNameBlur, handleNameChange }; }; -export const useGetBeginNodeDataQuery = () => { - const getNode = useGraphStore((state) => state.getNode); - - const getBeginNodeDataQuery = useCallback(() => { - return get(getNode('begin'), 'data.form.query', []); - }, [getNode]); - - return getBeginNodeDataQuery; -}; - -export const useGetBeginNodeDataQueryIsEmpty = () => { - const [isBeginNodeDataQueryEmpty, setIsBeginNodeDataQueryEmpty] = - useState(false); - const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - const nodes = useGraphStore((state) => state.nodes); - - useEffect(() => { - const query: BeginQuery[] = getBeginNodeDataQuery(); - setIsBeginNodeDataQueryEmpty(query.length === 0); - }, [getBeginNodeDataQuery, nodes]); - - return isBeginNodeDataQueryEmpty; -}; - -export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { - const { saveGraph, loading } = useSaveGraph(); - const { resetFlow } = useResetFlow(); - - const handleRun = useCallback( - async (nextNodes?: Node[]) => { - const saveRet = await saveGraph(nextNodes); - if (saveRet?.code === 0) { - // Call the reset api before opening the run drawer each time - const resetRet = await resetFlow(); - // After resetting, all previous messages will be cleared. - if (resetRet?.code === 0) { - show(); - } - } - }, - [saveGraph, resetFlow, show], - ); - - return { handleRun, loading }; -}; - export const useReplaceIdWithName = () => { const getNode = useGraphStore((state) => state.getNode); @@ -647,66 +531,6 @@ export const useWatchNodeFormDataChange = () => { ]); }; -// exclude nodes with branches -const ExcludedNodes = [ - Operator.Categorize, - Operator.Relevant, - Operator.Begin, - Operator.Note, -]; - -export const useBuildComponentIdSelectOptions = (nodeId?: string) => { - const nodes = useGraphStore((state) => state.nodes); - const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); - const query: BeginQuery[] = getBeginNodeDataQuery(); - - const componentIdOptions = useMemo(() => { - return nodes - .filter( - (x) => - x.id !== nodeId && !ExcludedNodes.some((y) => y === x.data.label), - ) - .map((x) => ({ label: x.data.name, value: x.id })); - }, [nodes, nodeId]); - - const groupedOptions = [ - { - label: Component Output, - title: 'Component Output', - options: componentIdOptions, - }, - { - label: Begin Input, - title: 'Begin Input', - options: query.map((x) => ({ - label: x.name, - value: `begin@${x.key}`, - })), - }, - ]; - - return groupedOptions; -}; - -export const useGetComponentLabelByValue = (nodeId: string) => { - const options = useBuildComponentIdSelectOptions(nodeId); - const flattenOptions = useMemo( - () => - options.reduce((pre, cur) => { - return [...pre, ...cur.options]; - }, []), - [options], - ); - - const getLabel = useCallback( - (val?: string) => { - return flattenOptions.find((x) => x.value === val)?.label; - }, - [flattenOptions], - ); - return getLabel; -}; - export const useDuplicateNode = () => { const duplicateNodeById = useGraphStore((store) => store.duplicateNode); const getNodeName = useGetNodeName(); @@ -769,107 +593,3 @@ export const useCopyPaste = () => { }; }, [onPasteCapture]); }; - -export const useWatchAgentChange = (chatDrawerVisible: boolean) => { - const [time, setTime] = useState(); - const nodes = useGraphStore((state) => state.nodes); - const edges = useGraphStore((state) => state.edges); - const { saveGraph } = useSaveGraph(); - const { data: flowDetail } = useFetchFlow(); - - const setSaveTime = useCallback((updateTime: number) => { - setTime(dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')); - }, []); - - useEffect(() => { - setSaveTime(flowDetail?.update_time); - }, [flowDetail, setSaveTime]); - - const saveAgent = useCallback(async () => { - if (!chatDrawerVisible) { - const ret = await saveGraph(); - setSaveTime(ret.data.update_time); - } - }, [chatDrawerVisible, saveGraph, setSaveTime]); - - useDebounceEffect( - () => { - saveAgent(); - }, - [nodes, edges], - { - wait: 1000 * 20, - }, - ); - - return time; -}; - -export const useHandleExportOrImportJsonFile = () => { - const { buildDslData } = useBuildDslData(); - const { - visible: fileUploadVisible, - hideModal: hideFileUploadModal, - showModal: showFileUploadModal, - } = useSetModalState(); - const setGraphInfo = useSetGraphInfo(); - const { data } = useFetchFlow(); - const { t } = useTranslation(); - - const onFileUploadOk = useCallback( - async (fileList: UploadFile[]) => { - if (fileList.length > 0) { - const file: File = fileList[0] as unknown as File; - if (file.type !== FileMimeType.Json) { - message.error(t('flow.jsonUploadTypeErrorMessage')); - return; - } - - const graphStr = await file.text(); - const errorMessage = t('flow.jsonUploadContentErrorMessage'); - try { - const graph = JSON.parse(graphStr); - if (graphStr && !isEmpty(graph) && Array.isArray(graph?.nodes)) { - setGraphInfo(graph ?? ({} as IGraph)); - hideFileUploadModal(); - } else { - message.error(errorMessage); - } - } catch (error) { - message.error(errorMessage); - } - } - }, - [hideFileUploadModal, setGraphInfo, t], - ); - - const handleExportJson = useCallback(() => { - downloadJsonFile(buildDslData().graph, `${data.title}.json`); - }, [buildDslData, data.title]); - - return { - fileUploadVisible, - handleExportJson, - handleImportJson: showFileUploadModal, - hideFileUploadModal, - onFileUploadOk, - }; -}; - -export const useShowSingleDebugDrawer = () => { - const { visible, showModal, hideModal } = useSetModalState(); - const { saveGraph } = useSaveGraph(); - - const showSingleDebugDrawer = useCallback(async () => { - const saveRet = await saveGraph(); - if (saveRet?.code === 0) { - showModal(); - } - }, [saveGraph, showModal]); - - return { - singleDebugDrawerVisible: visible, - hideSingleDebugDrawer: hideModal, - showSingleDebugDrawer, - }; -}; diff --git a/web/src/pages/flow/hooks/use-build-dsl.ts b/web/src/pages/flow/hooks/use-build-dsl.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6e5c015ba07bb821328c3ac468722b544c5c3ef --- /dev/null +++ b/web/src/pages/flow/hooks/use-build-dsl.ts @@ -0,0 +1,29 @@ +import { useFetchFlow } from '@/hooks/flow-hooks'; +import { useCallback } from 'react'; +import { Node } from 'reactflow'; +import useGraphStore from '../store'; +import { buildDslComponentsByGraph } from '../utils'; + +export const useBuildDslData = () => { + const { data } = useFetchFlow(); + const { nodes, edges } = useGraphStore((state) => state); + + const buildDslData = useCallback( + (currentNodes?: Node[]) => { + const dslComponents = buildDslComponentsByGraph( + currentNodes ?? nodes, + edges, + data.dsl.components, + ); + + return { + ...data.dsl, + graph: { nodes: currentNodes ?? nodes, edges }, + components: dslComponents, + }; + }, + [data.dsl, edges, nodes], + ); + + return { buildDslData }; +}; diff --git a/web/src/pages/flow/hooks/use-export-json.ts b/web/src/pages/flow/hooks/use-export-json.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b8618de8c87aef2ff2f934ed763f9c62c15dcd9 --- /dev/null +++ b/web/src/pages/flow/hooks/use-export-json.ts @@ -0,0 +1,62 @@ +import { FileMimeType } from '@/constants/common'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { useFetchFlow } from '@/hooks/flow-hooks'; +import { IGraph } from '@/interfaces/database/flow'; +import { downloadJsonFile } from '@/utils/file-util'; +import { message, UploadFile } from 'antd'; +import isEmpty from 'lodash/isEmpty'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBuildDslData } from './use-build-dsl'; +import { useSetGraphInfo } from './use-set-graph'; + +export const useHandleExportOrImportJsonFile = () => { + const { buildDslData } = useBuildDslData(); + const { + visible: fileUploadVisible, + hideModal: hideFileUploadModal, + showModal: showFileUploadModal, + } = useSetModalState(); + const setGraphInfo = useSetGraphInfo(); + const { data } = useFetchFlow(); + const { t } = useTranslation(); + + const onFileUploadOk = useCallback( + async (fileList: UploadFile[]) => { + if (fileList.length > 0) { + const file: File = fileList[0] as unknown as File; + if (file.type !== FileMimeType.Json) { + message.error(t('flow.jsonUploadTypeErrorMessage')); + return; + } + + const graphStr = await file.text(); + const errorMessage = t('flow.jsonUploadContentErrorMessage'); + try { + const graph = JSON.parse(graphStr); + if (graphStr && !isEmpty(graph) && Array.isArray(graph?.nodes)) { + setGraphInfo(graph ?? ({} as IGraph)); + hideFileUploadModal(); + } else { + message.error(errorMessage); + } + } catch (error) { + message.error(errorMessage); + } + } + }, + [hideFileUploadModal, setGraphInfo, t], + ); + + const handleExportJson = useCallback(() => { + downloadJsonFile(buildDslData().graph, `${data.title}.json`); + }, [buildDslData, data.title]); + + return { + fileUploadVisible, + handleExportJson, + handleImportJson: showFileUploadModal, + hideFileUploadModal, + onFileUploadOk, + }; +}; diff --git a/web/src/pages/flow/hooks/use-fetch-data.ts b/web/src/pages/flow/hooks/use-fetch-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..245ea6abf83adcf243c29cc768a47d477ec14477 --- /dev/null +++ b/web/src/pages/flow/hooks/use-fetch-data.ts @@ -0,0 +1,19 @@ +import { useFetchFlow } from '@/hooks/flow-hooks'; +import { IGraph } from '@/interfaces/database/flow'; +import { useEffect } from 'react'; +import { useSetGraphInfo } from './use-set-graph'; + +export const useFetchDataOnMount = () => { + const { loading, data, refetch } = useFetchFlow(); + const setGraphInfo = useSetGraphInfo(); + + useEffect(() => { + setGraphInfo(data?.dsl?.graph ?? ({} as IGraph)); + }, [setGraphInfo, data]); + + useEffect(() => { + refetch(); + }, [refetch]); + + return { loading, flowDetail: data }; +}; diff --git a/web/src/pages/flow/hooks/use-get-begin-query.tsx b/web/src/pages/flow/hooks/use-get-begin-query.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a86affaf44eeb7c4838bcf2495a585e8dcc45f9 --- /dev/null +++ b/web/src/pages/flow/hooks/use-get-begin-query.tsx @@ -0,0 +1,112 @@ +import { DefaultOptionType } from 'antd/es/select'; +import get from 'lodash/get'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Node } from 'reactflow'; +import { BeginId, Operator } from '../constant'; +import { BeginQuery, NodeData } from '../interface'; +import useGraphStore from '../store'; + +export const useGetBeginNodeDataQuery = () => { + const getNode = useGraphStore((state) => state.getNode); + + const getBeginNodeDataQuery = useCallback(() => { + return get(getNode(BeginId), 'data.form.query', []); + }, [getNode]); + + return getBeginNodeDataQuery; +}; + +export const useGetBeginNodeDataQueryIsEmpty = () => { + const [isBeginNodeDataQueryEmpty, setIsBeginNodeDataQueryEmpty] = + useState(false); + const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); + const nodes = useGraphStore((state) => state.nodes); + + useEffect(() => { + const query: BeginQuery[] = getBeginNodeDataQuery(); + setIsBeginNodeDataQueryEmpty(query.length === 0); + }, [getBeginNodeDataQuery, nodes]); + + return isBeginNodeDataQueryEmpty; +}; + +// exclude nodes with branches +const ExcludedNodes = [ + Operator.Categorize, + Operator.Relevant, + Operator.Begin, + Operator.Note, +]; + +export const useBuildComponentIdSelectOptions = ( + nodeId?: string, + parentId?: string, +) => { + const nodes = useGraphStore((state) => state.nodes); + const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); + const query: BeginQuery[] = getBeginNodeDataQuery(); + + // Limit the nodes inside iteration to only reference peer nodes with the same parentId and other external nodes other than their parent nodes + const filterChildNodesToSameParentOrExternal = useCallback( + (node: Node) => { + // Node inside iteration + if (parentId) { + return ( + (node.parentId === parentId || node.parentId === undefined) && + node.id !== parentId + ); + } + + return node.parentId === undefined; // The outermost node + }, + [parentId], + ); + + const componentIdOptions = useMemo(() => { + return nodes + .filter( + (x) => + x.id !== nodeId && + !ExcludedNodes.some((y) => y === x.data.label) && + filterChildNodesToSameParentOrExternal(x), + ) + .map((x) => ({ label: x.data.name, value: x.id })); + }, [nodes, nodeId, filterChildNodesToSameParentOrExternal]); + + const groupedOptions = [ + { + label: Component Output, + title: 'Component Output', + options: componentIdOptions, + }, + { + label: Begin Input, + title: 'Begin Input', + options: query.map((x) => ({ + label: x.name, + value: `begin@${x.key}`, + })), + }, + ]; + + return groupedOptions; +}; + +export const useGetComponentLabelByValue = (nodeId: string) => { + const options = useBuildComponentIdSelectOptions(nodeId); + const flattenOptions = useMemo( + () => + options.reduce((pre, cur) => { + return [...pre, ...cur.options]; + }, []), + [options], + ); + + const getLabel = useCallback( + (val?: string) => { + return flattenOptions.find((x) => x.value === val)?.label; + }, + [flattenOptions], + ); + return getLabel; +}; diff --git a/web/src/pages/flow/hooks/use-iteration.ts b/web/src/pages/flow/hooks/use-iteration.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/web/src/pages/flow/hooks/use-save-graph.ts b/web/src/pages/flow/hooks/use-save-graph.ts new file mode 100644 index 0000000000000000000000000000000000000000..e042aca6251b1f194e1aa86e6b965c3e130cd508 --- /dev/null +++ b/web/src/pages/flow/hooks/use-save-graph.ts @@ -0,0 +1,85 @@ +import { useFetchFlow, useResetFlow, useSetFlow } from '@/hooks/flow-hooks'; +import { useDebounceEffect } from 'ahooks'; +import dayjs from 'dayjs'; +import { useCallback, useEffect, useState } from 'react'; +import { Node } from 'reactflow'; +import { useParams } from 'umi'; +import useGraphStore from '../store'; +import { useBuildDslData } from './use-build-dsl'; + +export const useSaveGraph = () => { + const { data } = useFetchFlow(); + const { setFlow, loading } = useSetFlow(); + const { id } = useParams(); + const { buildDslData } = useBuildDslData(); + + const saveGraph = useCallback( + async (currentNodes?: Node[]) => { + return setFlow({ + id, + title: data.title, + dsl: buildDslData(currentNodes), + }); + }, + [setFlow, id, data.title, buildDslData], + ); + + return { saveGraph, loading }; +}; + +export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { + const { saveGraph, loading } = useSaveGraph(); + const { resetFlow } = useResetFlow(); + + const handleRun = useCallback( + async (nextNodes?: Node[]) => { + const saveRet = await saveGraph(nextNodes); + if (saveRet?.code === 0) { + // Call the reset api before opening the run drawer each time + const resetRet = await resetFlow(); + // After resetting, all previous messages will be cleared. + if (resetRet?.code === 0) { + show(); + } + } + }, + [saveGraph, resetFlow, show], + ); + + return { handleRun, loading }; +}; + +export const useWatchAgentChange = (chatDrawerVisible: boolean) => { + const [time, setTime] = useState(); + const nodes = useGraphStore((state) => state.nodes); + const edges = useGraphStore((state) => state.edges); + const { saveGraph } = useSaveGraph(); + const { data: flowDetail } = useFetchFlow(); + + const setSaveTime = useCallback((updateTime: number) => { + setTime(dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')); + }, []); + + useEffect(() => { + setSaveTime(flowDetail?.update_time); + }, [flowDetail, setSaveTime]); + + const saveAgent = useCallback(async () => { + if (!chatDrawerVisible) { + const ret = await saveGraph(); + setSaveTime(ret.data.update_time); + } + }, [chatDrawerVisible, saveGraph, setSaveTime]); + + useDebounceEffect( + () => { + saveAgent(); + }, + [nodes, edges], + { + wait: 1000 * 20, + }, + ); + + return time; +}; diff --git a/web/src/pages/flow/hooks/use-set-graph.ts b/web/src/pages/flow/hooks/use-set-graph.ts new file mode 100644 index 0000000000000000000000000000000000000000..6dd68a330d4b063a5da8a500360d4742c70139cc --- /dev/null +++ b/web/src/pages/flow/hooks/use-set-graph.ts @@ -0,0 +1,17 @@ +import { IGraph } from '@/interfaces/database/flow'; +import { useCallback } from 'react'; +import useGraphStore from '../store'; + +export const useSetGraphInfo = () => { + const { setEdges, setNodes } = useGraphStore((state) => state); + const setGraphInfo = useCallback( + ({ nodes = [], edges = [] }: IGraph) => { + if (nodes.length || edges.length) { + setNodes(nodes); + setEdges(edges); + } + }, + [setEdges, setNodes], + ); + return setGraphInfo; +}; diff --git a/web/src/pages/flow/hooks/use-show-drawer.tsx b/web/src/pages/flow/hooks/use-show-drawer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8146db4bcf8f35f07383fc8c044df66a36164e43 --- /dev/null +++ b/web/src/pages/flow/hooks/use-show-drawer.tsx @@ -0,0 +1,153 @@ +import { useSetModalState } from '@/hooks/common-hooks'; +import get from 'lodash/get'; +import { useCallback, useEffect } from 'react'; +import { Node, NodeMouseHandler } from 'reactflow'; +import { Operator } from '../constant'; +import { BeginQuery } from '../interface'; +import useGraphStore from '../store'; +import { useGetBeginNodeDataQuery } from './use-get-begin-query'; +import { useSaveGraph } from './use-save-graph'; + +export const useShowFormDrawer = () => { + const { + clickedNodeId: clickNodeId, + setClickedNodeId, + getNode, + } = useGraphStore((state) => state); + const { + visible: formDrawerVisible, + hideModal: hideFormDrawer, + showModal: showFormDrawer, + } = useSetModalState(); + + const handleShow = useCallback( + (node: Node) => { + setClickedNodeId(node.id); + showFormDrawer(); + }, + [showFormDrawer, setClickedNodeId], + ); + + return { + formDrawerVisible, + hideFormDrawer, + showFormDrawer: handleShow, + clickedNode: getNode(clickNodeId), + }; +}; + +export const useShowSingleDebugDrawer = () => { + const { visible, showModal, hideModal } = useSetModalState(); + const { saveGraph } = useSaveGraph(); + + const showSingleDebugDrawer = useCallback(async () => { + const saveRet = await saveGraph(); + if (saveRet?.code === 0) { + showModal(); + } + }, [saveGraph, showModal]); + + return { + singleDebugDrawerVisible: visible, + hideSingleDebugDrawer: hideModal, + showSingleDebugDrawer, + }; +}; + +const ExcludedNodes = [Operator.IterationStart, Operator.Note]; + +export function useShowDrawer({ + drawerVisible, + hideDrawer, +}: { + drawerVisible: boolean; + hideDrawer(): void; +}) { + const { + visible: runVisible, + showModal: showRunModal, + hideModal: hideRunModal, + } = useSetModalState(); + const { + visible: chatVisible, + showModal: showChatModal, + hideModal: hideChatModal, + } = useSetModalState(); + const { + singleDebugDrawerVisible, + showSingleDebugDrawer, + hideSingleDebugDrawer, + } = useShowSingleDebugDrawer(); + const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } = + useShowFormDrawer(); + const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); + + useEffect(() => { + if (drawerVisible) { + const query: BeginQuery[] = getBeginNodeDataQuery(); + if (query.length > 0) { + showRunModal(); + hideChatModal(); + } else { + showChatModal(); + hideRunModal(); + } + } + }, [ + hideChatModal, + hideRunModal, + showChatModal, + showRunModal, + drawerVisible, + getBeginNodeDataQuery, + ]); + + const hideRunOrChatDrawer = useCallback(() => { + hideChatModal(); + hideRunModal(); + hideDrawer(); + }, [hideChatModal, hideDrawer, hideRunModal]); + + const onPaneClick = useCallback(() => { + hideFormDrawer(); + }, [hideFormDrawer]); + + const onNodeClick: NodeMouseHandler = useCallback( + (e, node) => { + if (!ExcludedNodes.some((x) => x === node.data.label)) { + hideSingleDebugDrawer(); + hideRunOrChatDrawer(); + showFormDrawer(node); + } + // handle single debug icon click + if ( + get(e.target, 'dataset.play') === 'true' || + get(e.target, 'parentNode.dataset.play') === 'true' + ) { + showSingleDebugDrawer(); + } + }, + [ + hideRunOrChatDrawer, + hideSingleDebugDrawer, + showFormDrawer, + showSingleDebugDrawer, + ], + ); + + return { + chatVisible, + runVisible, + onPaneClick, + singleDebugDrawerVisible, + showSingleDebugDrawer, + hideSingleDebugDrawer, + formDrawerVisible, + showFormDrawer, + clickedNode, + onNodeClick, + hideFormDrawer, + hideRunOrChatDrawer, + showChatModal, + }; +} diff --git a/web/src/pages/flow/index.tsx b/web/src/pages/flow/index.tsx index 82dd856af2547ea320982ea3e022fe42b228af41..50b6256832ffac9d36dfb815b58a6e3a28f1e87f 100644 --- a/web/src/pages/flow/index.tsx +++ b/web/src/pages/flow/index.tsx @@ -5,7 +5,8 @@ import { ReactFlowProvider } from 'reactflow'; import FlowCanvas from './canvas'; import Sider from './flow-sider'; import FlowHeader from './header'; -import { useCopyPaste, useFetchDataOnMount } from './hooks'; +import { useCopyPaste } from './hooks'; +import { useFetchDataOnMount } from './hooks/use-fetch-data'; const { Content } = Layout; diff --git a/web/src/pages/flow/interface.ts b/web/src/pages/flow/interface.ts index d0b80a24cb572ac213aab00973be41972482ac11..3975500f73c208771d39e18020a3272ae8e2cb6b 100644 --- a/web/src/pages/flow/interface.ts +++ b/web/src/pages/flow/interface.ts @@ -90,7 +90,7 @@ export interface ISwitchForm { export type NodeData = { label: string; // operator type name: string; // operator name - color: string; + color?: string; form: | IBeginForm | IRetrievalForm diff --git a/web/src/pages/flow/list/hooks.ts b/web/src/pages/flow/list/hooks.ts index f82d9c0b65bc4277b4c4d34366d73b7fd6241a8e..219dfa0e1ac989e90812fe6aeb6a1b9bddaf3ade 100644 --- a/web/src/pages/flow/list/hooks.ts +++ b/web/src/pages/flow/list/hooks.ts @@ -4,18 +4,8 @@ import { useFetchFlowTemplates, useSetFlow, } from '@/hooks/flow-hooks'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { useNavigate } from 'umi'; -// import { dsl } from '../mock'; -// import headhunterZhComponents from '../../../../../graph/test/dsl_examples/headhunter_zh.json'; -// import dslJson from '../../../../../dls.json'; -// import customerServiceBase from '../../../../../graph/test/dsl_examples/customer_service.json'; -// import customerService from '../customer_service.json'; -// import interpreterBase from '../../../../../graph/test/dsl_examples/interpreter.json'; -// import interpreter from '../interpreter.json'; - -// import retrievalRelevantRewriteAndGenerateBase from '../../../../../graph/test/dsl_examples/retrieval_relevant_rewrite_and_generate.json'; -// import retrievalRelevantRewriteAndGenerate from '../retrieval_relevant_rewrite_and_generate.json'; export const useFetchDataOnMount = () => { const { data, loading } = useFetchFlowList(); @@ -24,7 +14,6 @@ export const useFetchDataOnMount = () => { }; export const useSaveFlow = () => { - const [currentFlow, setCurrentFlow] = useState({}); const { visible: flowSettingVisible, hideModal: hideFlowSettingModal, @@ -39,18 +28,10 @@ export const useSaveFlow = () => { const templateItem = list.find((x) => x.id === templateId); let dsl = templateItem?.dsl; - // if (dsl) { - // dsl.graph = headhunter_zh; - // } const ret = await setFlow({ title, dsl, avatar: templateItem?.avatar, - // dsl: dslJson, - // dsl: { - // ...retrievalRelevantRewriteAndGenerateBase, - // graph: retrievalRelevantRewriteAndGenerate, - // }, }); if (ret?.code === 0) { @@ -61,20 +42,12 @@ export const useSaveFlow = () => { [setFlow, hideFlowSettingModal, navigate, list], ); - const handleShowFlowSettingModal = useCallback( - async (record: any) => { - setCurrentFlow(record); - showFileRenameModal(); - }, - [showFileRenameModal], - ); - return { flowSettingLoading: loading, initialFlowName: '', onFlowOk, flowSettingVisible, hideFlowSettingModal, - showFlowSettingModal: handleShowFlowSettingModal, + showFlowSettingModal: showFileRenameModal, }; }; diff --git a/web/src/pages/flow/run-drawer/index.tsx b/web/src/pages/flow/run-drawer/index.tsx index 063c5ad47f02c0cf297a91e462e43674bb451bbd..d6cf4593a046bee3325a451b05a8b879d77decdf 100644 --- a/web/src/pages/flow/run-drawer/index.tsx +++ b/web/src/pages/flow/run-drawer/index.tsx @@ -2,16 +2,14 @@ import { IModalProps } from '@/interfaces/common'; import { Drawer } from 'antd'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { - useGetBeginNodeDataQuery, - useSaveGraphBeforeOpeningDebugDrawer, -} from '../hooks'; +import { BeginId } from '../constant'; +import DebugContent from '../debug-content'; +import { useGetBeginNodeDataQuery } from '../hooks/use-get-begin-query'; +import { useSaveGraphBeforeOpeningDebugDrawer } from '../hooks/use-save-graph'; import { BeginQuery } from '../interface'; import useGraphStore from '../store'; import { getDrawerWidth } from '../utils'; -import DebugContent from '../debug-content'; - const RunDrawer = ({ hideModal, showModal: showChatModal, @@ -28,7 +26,7 @@ const RunDrawer = ({ const handleRunAgent = useCallback( (nextValues: Record) => { - const currentNodes = updateNodeForm('begin', nextValues, ['query']); + const currentNodes = updateNodeForm(BeginId, nextValues, ['query']); handleRun(currentNodes); hideModal?.(); }, diff --git a/web/src/pages/flow/store.ts b/web/src/pages/flow/store.ts index 54fccb9a0fea7b8997e89e2b376b4c4f267cc24a..3f645a406736bbd5453b30a05e16488b5d628167 100644 --- a/web/src/pages/flow/store.ts +++ b/web/src/pages/flow/store.ts @@ -1,5 +1,5 @@ import type {} from '@redux-devtools/extension'; -import { humanId } from 'human-id'; +import { omit } from 'lodash'; import differenceWith from 'lodash/differenceWith'; import intersectionWith from 'lodash/intersectionWith'; import lodashSet from 'lodash/set'; @@ -25,8 +25,8 @@ import { Operator, SwitchElseTo } from './constant'; import { NodeData } from './interface'; import { duplicateNodeForm, + generateDuplicateNode, generateNodeNamesWithIncreasingIndex, - getNodeDragHandle, getOperatorIndex, isEdgeEqual, } from './utils'; @@ -61,13 +61,16 @@ export type RFState = { ) => void; deletePreviousEdgeOfClassificationNode: (connection: Connection) => void; duplicateNode: (id: string, name: string) => void; + duplicateIterationNode: (id: string, name: string) => void; deleteEdge: () => void; deleteEdgeById: (id: string) => void; deleteNodeById: (id: string) => void; + deleteIterationNodeById: (id: string) => void; deleteEdgeBySourceAndSourceHandle: (connection: Partial) => void; findNodeByName: (operatorName: Operator) => Node | undefined; updateMutableNodeFormItem: (id: string, field: string, value: any) => void; getOperatorTypeFromId: (id?: string | null) => string | undefined; + getParentIdById: (id?: string | null) => string | undefined; updateNodeName: (id: string, name: string) => void; generateNodeName: (name: string) => string; setClickedNodeId: (id?: string) => void; @@ -170,6 +173,9 @@ const useGraphStore = create()( getOperatorTypeFromId: (id?: string | null) => { return get().getNode(id)?.data?.label; }, + getParentIdById: (id?: string | null) => { + return get().getNode(id)?.parentId; + }, addEdge: (connection: Connection) => { set({ edges: addEdge(connection, get().edges), @@ -234,12 +240,14 @@ const useGraphStore = create()( } }, duplicateNode: (id: string, name: string) => { - const { getNode, addNode, generateNodeName } = get(); + const { getNode, addNode, generateNodeName, duplicateIterationNode } = + get(); const node = getNode(id); - const position = { - x: (node?.position?.x || 0) + 50, - y: (node?.position?.y || 0) + 50, - }; + + if (node?.data.label === Operator.Iteration) { + duplicateIterationNode(id, name); + return; + } addNode({ ...(node || {}), @@ -247,13 +255,38 @@ const useGraphStore = create()( ...duplicateNodeForm(node?.data), name: generateNodeName(name), }, - selected: false, - dragging: false, - id: `${node?.data?.label}:${humanId()}`, - position, - dragHandle: getNodeDragHandle(node?.data?.label), + ...generateDuplicateNode(node?.position, node?.data?.label), }); }, + duplicateIterationNode: (id: string, name: string) => { + const { getNode, generateNodeName, nodes } = get(); + const node = getNode(id); + + const iterationNode: Node = { + ...(node || {}), + data: { + ...(node?.data || { label: Operator.Iteration, form: {} }), + name: generateNodeName(name), + }, + ...generateDuplicateNode(node?.position, node?.data?.label), + }; + + const children = nodes + .filter((x) => x.parentId === node?.id) + .map((x) => ({ + ...(x || {}), + data: { + ...duplicateNodeForm(x?.data), + name: generateNodeName(x.data.name), + }, + ...omit(generateDuplicateNode(x?.position, x?.data?.label), [ + 'position', + ]), + parentId: iterationNode.id, + })); + + set({ nodes: nodes.concat(iterationNode, ...children) }); + }, deleteEdge: () => { const { edges, selectedEdgeIds } = get(); set({ @@ -323,6 +356,21 @@ const useGraphStore = create()( .filter((edge) => edge.target !== id), }); }, + deleteIterationNodeById: (id: string) => { + const { nodes, edges } = get(); + const children = nodes.filter((node) => node.parentId === id); + set({ + nodes: nodes.filter((node) => node.id !== id && node.parentId !== id), + edges: edges.filter( + (edge) => + edge.source !== id && + edge.target !== id && + !children.some( + (child) => edge.source === child.id && edge.target === child.id, + ), + ), + }); + }, findNodeByName: (name: Operator) => { return get().nodes.find((x) => x.data.label === name); }, diff --git a/web/src/pages/flow/utils.ts b/web/src/pages/flow/utils.ts index a4662109553690231773c8e05df85e7c4776adf1..6585d24089b16daec79d0cec164f2039d8f742e5 100644 --- a/web/src/pages/flow/utils.ts +++ b/web/src/pages/flow/utils.ts @@ -5,7 +5,7 @@ import { humanId } from 'human-id'; import { curry, get, intersectionWith, isEqual, sample } from 'lodash'; import pipe from 'lodash/fp/pipe'; import isObject from 'lodash/isObject'; -import { Edge, Node, Position } from 'reactflow'; +import { Edge, Node, Position, XYPosition } from 'reactflow'; import { v4 as uuidv4 } from 'uuid'; import { CategorizeAnchorPointPositions, @@ -144,6 +144,7 @@ export const buildDslComponentsByGraph = ( }, downstream: buildComponentDownstreamOrUpstream(edges, id, true), upstream: buildComponentDownstreamOrUpstream(edges, id, false), + parent_id: x?.parentId, }; }); @@ -332,3 +333,55 @@ export const getDrawerWidth = () => { export const needsSingleStepDebugging = (label: string) => { return !NoDebugOperatorsList.some((x) => (label as Operator) === x); }; + +// Get the coordinates of the node relative to the Iteration node +export function getRelativePositionToIterationNode( + nodes: Node[], + position?: XYPosition, // relative position +) { + if (!position) { + return; + } + + const iterationNodes = nodes.filter( + (node) => node.data.label === Operator.Iteration, + ); + + for (const iterationNode of iterationNodes) { + const { + position: { x, y }, + width, + height, + } = iterationNode; + const halfWidth = (width || 0) / 2; + if ( + position.x >= x - halfWidth && + position.x <= x + halfWidth && + position.y >= y && + position.y <= y + (height || 0) + ) { + return { + parentId: iterationNode.id, + position: { x: position.x - x + halfWidth, y: position.y - y }, + }; + } + } +} + +export const generateDuplicateNode = ( + position?: XYPosition, + label?: string, +) => { + const nextPosition = { + x: (position?.x || 0) + 50, + y: (position?.y || 0) + 50, + }; + + return { + selected: false, + dragging: false, + id: `${label}:${humanId()}`, + position: nextPosition, + dragHandle: getNodeDragHandle(label), + }; +}; diff --git a/web/src/pages/knowledge/index.tsx b/web/src/pages/knowledge/index.tsx index 3596c91f1b38f319a241fb475dff2ac9389dfb29..41b45e99e4b4431c0d8f979a57673b9c9fdfb62d 100644 --- a/web/src/pages/knowledge/index.tsx +++ b/web/src/pages/knowledge/index.tsx @@ -38,7 +38,6 @@ const KnowledgeList = () => { handleInputChange, loading, } = useInfiniteFetchKnowledgeList(); - console.log('🚀 ~ KnowledgeList ~ data:', data); const nextList = data?.pages?.flatMap((x) => x.kbs) ?? []; const total = useMemo(() => { diff --git a/web/src/pages/workflow.less b/web/src/pages/workflow.less new file mode 100644 index 0000000000000000000000000000000000000000..975ebb49fe7e942b95f031bb569393eb39d39c9a --- /dev/null +++ b/web/src/pages/workflow.less @@ -0,0 +1,5 @@ +.react-flow-subflows-example { + .react-flow__node-group { + padding: 0; + } +} diff --git a/web/src/pages/workflow.tsx b/web/src/pages/workflow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f6b609e32237d3ec7a8f81d5fb8cfad6235091ef --- /dev/null +++ b/web/src/pages/workflow.tsx @@ -0,0 +1,151 @@ +import { useCallback } from 'react'; +import ReactFlow, { + Background, + Controls, + Handle, + MiniMap, + NodeProps, + Position, + addEdge, + useEdgesState, + useNodesState, +} from 'reactflow'; +import 'reactflow/dist/style.css'; + +import './workflow.less'; + +const initialNodes = [ + { + id: '1', + type: 'input', + data: { label: 'Node 0' }, + position: { x: 250, y: 5 }, + className: 'light', + }, + { + id: '2', + data: { label: 'Group A' }, + position: { x: 100, y: 100 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, 0.2)', width: 200, height: 200 }, + }, + { + id: '2a', + data: { label: 'Node A.1' }, + position: { x: 10, y: 50 }, + parentId: '2', + }, + { + id: '3', + data: { label: 'Node 1' }, + position: { x: 320, y: 100 }, + className: 'light', + }, + { + id: '4', + data: { label: 'Group B' }, + position: { x: 320, y: 200 }, + className: 'light', + style: { backgroundColor: 'rgba(255, 0, 0, 0.2)', width: 300, height: 300 }, + type: 'group', + }, + { + id: '4a', + data: { label: 'Node B.1' }, + position: { x: 15, y: 65 }, + className: 'light', + parentId: '4', + extent: 'parent', + draggable: false, + }, + { + id: '4b', + data: { label: 'Group B.A' }, + position: { x: 15, y: 120 }, + className: 'light', + style: { + backgroundColor: 'rgba(255, 0, 255, 0.2)', + height: 150, + width: 270, + }, + parentId: '4', + }, + { + id: '4b1', + data: { label: 'Node B.A.1' }, + position: { x: 20, y: 40 }, + className: 'light', + parentId: '4b', + }, + { + id: '4b2', + data: { label: 'Node B.A.2' }, + position: { x: 100, y: 100 }, + className: 'light', + parentId: '4b', + }, +]; + +const initialEdges = [ + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, + { id: 'e2a-4a', source: '2a', target: '4a' }, + { id: 'e3-4b', source: '3', target: '4b' }, + { id: 'e4a-4b1', source: '4a', target: '4b1' }, + { id: 'e4a-4b2', source: '4a', target: '4b2' }, + { id: 'e4b1-4b2', source: '4b1', target: '4b2' }, +]; + +export function RagNode({ id, data, isConnectable = true }: NodeProps) { + return ( +
+
header
+ + +
xxx
+
+ ); +} + +const nodeTypes = { group: RagNode }; + +const NestedFlow = () => { + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const onConnect = useCallback((connection) => { + setEdges((eds) => addEdge(connection, eds)); + }, []); + + return ( + { + console.log(node); + }} + nodeTypes={nodeTypes} + > + + + + + ); +}; + +export default NestedFlow; diff --git a/web/src/routes.ts b/web/src/routes.ts index 456fa08440ee5039c03035413b2244dfe405a45f..801d77a9b048a8db3d9eeca3e90a1b301709c958 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -246,6 +246,11 @@ const routes = [ }, ], }, + { + path: '/workflow', + component: '@/pages/workflow', + layout: false, + }, ]; export default routes;