From a1a825c830cf143ecdadd3e74775ae1ff175bbc1 Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 27 Dec 2024 11:24:17 +0800 Subject: [PATCH] Feat: Add the iteration Node #4242 (#4247) ### What problem does this PR solve? Feat: Add the iteration Node #4242 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/assets/icon/Icon.tsx | 38 ++ web/src/components/delimiter.tsx | 11 +- web/src/interfaces/database/flow.ts | 1 + web/src/less/mixins.less | 20 + web/src/locales/en.ts | 16 + web/src/locales/zh-traditional.ts | 14 + web/src/locales/zh.ts | 14 + web/src/pages/flow/canvas/edge/index.tsx | 1 + web/src/pages/flow/canvas/index.less | 6 + web/src/pages/flow/canvas/index.tsx | 105 +---- web/src/pages/flow/canvas/node/begin-node.tsx | 4 +- web/src/pages/flow/canvas/node/dropdown.tsx | 12 +- .../pages/flow/canvas/node/generate-node.tsx | 2 +- web/src/pages/flow/canvas/node/index.less | 40 +- .../pages/flow/canvas/node/iteration-node.tsx | 118 ++++++ .../pages/flow/canvas/node/node-header.tsx | 21 +- web/src/pages/flow/canvas/node/popover.tsx | 3 +- .../pages/flow/canvas/node/switch-node.tsx | 2 +- .../pages/flow/canvas/node/template-node.tsx | 4 +- web/src/pages/flow/constant.tsx | 23 + web/src/pages/flow/flow-drawer/index.tsx | 19 +- .../pages/flow/form/akshare-form/index.tsx | 2 +- web/src/pages/flow/form/arxiv-form/index.tsx | 2 +- .../flow/form/baidu-fanyi-form/index.tsx | 2 +- web/src/pages/flow/form/baidu-form/index.tsx | 2 +- web/src/pages/flow/form/bing-form/index.tsx | 2 +- .../pages/flow/form/categorize-form/index.tsx | 2 +- .../components/dynamic-input-variable.tsx | 19 +- .../pages/flow/form/crawler-form/index.tsx | 2 +- web/src/pages/flow/form/deepl-form/index.tsx | 2 +- .../pages/flow/form/duckduckgo-form/index.tsx | 2 +- web/src/pages/flow/form/email-form/index.tsx | 2 +- web/src/pages/flow/form/exesql-form/index.tsx | 2 +- .../form/generate-form/dynamic-parameters.tsx | 15 +- .../pages/flow/form/generate-form/index.tsx | 2 +- web/src/pages/flow/form/github-form/index.tsx | 2 +- web/src/pages/flow/form/google-form/index.tsx | 2 +- .../flow/form/google-scholar-form/index.tsx | 2 +- .../form/invoke-form/dynamic-variables.tsx | 14 +- web/src/pages/flow/form/invoke-form/index.tsx | 2 +- .../pages/flow/form/iteration-from/index.tsx | 94 +++++ web/src/pages/flow/form/jin10-form/index.tsx | 2 +- .../flow/form/keyword-extract-form/index.tsx | 2 +- web/src/pages/flow/form/pubmed-form/index.tsx | 2 +- .../pages/flow/form/qweather-form/index.tsx | 2 +- .../pages/flow/form/retrieval-form/index.tsx | 2 +- web/src/pages/flow/form/switch-form/index.tsx | 7 +- .../pages/flow/form/template-form/index.tsx | 2 +- .../pages/flow/form/tushare-form/index.tsx | 2 +- web/src/pages/flow/form/wencai-form/index.tsx | 2 +- .../pages/flow/form/wikipedia-form/index.tsx | 2 +- .../flow/form/yahoo-finance-form/index.tsx | 2 +- web/src/pages/flow/header/index.tsx | 5 +- web/src/pages/flow/hooks.tsx | 396 +++--------------- web/src/pages/flow/hooks/use-build-dsl.ts | 29 ++ web/src/pages/flow/hooks/use-export-json.ts | 62 +++ web/src/pages/flow/hooks/use-fetch-data.ts | 19 + .../pages/flow/hooks/use-get-begin-query.tsx | 112 +++++ web/src/pages/flow/hooks/use-iteration.ts | 0 web/src/pages/flow/hooks/use-save-graph.ts | 85 ++++ web/src/pages/flow/hooks/use-set-graph.ts | 17 + web/src/pages/flow/hooks/use-show-drawer.tsx | 153 +++++++ web/src/pages/flow/index.tsx | 3 +- web/src/pages/flow/interface.ts | 2 +- web/src/pages/flow/list/hooks.ts | 31 +- web/src/pages/flow/run-drawer/index.tsx | 12 +- web/src/pages/flow/store.ts | 72 +++- web/src/pages/flow/utils.ts | 55 ++- web/src/pages/knowledge/index.tsx | 1 - web/src/pages/workflow.less | 5 + web/src/pages/workflow.tsx | 151 +++++++ web/src/routes.ts | 5 + 72 files changed, 1330 insertions(+), 560 deletions(-) create mode 100644 web/src/pages/flow/canvas/node/iteration-node.tsx create mode 100644 web/src/pages/flow/form/iteration-from/index.tsx create mode 100644 web/src/pages/flow/hooks/use-build-dsl.ts create mode 100644 web/src/pages/flow/hooks/use-export-json.ts create mode 100644 web/src/pages/flow/hooks/use-fetch-data.ts create mode 100644 web/src/pages/flow/hooks/use-get-begin-query.tsx create mode 100644 web/src/pages/flow/hooks/use-iteration.ts create mode 100644 web/src/pages/flow/hooks/use-save-graph.ts create mode 100644 web/src/pages/flow/hooks/use-set-graph.ts create mode 100644 web/src/pages/flow/hooks/use-show-drawer.tsx create mode 100644 web/src/pages/workflow.less create mode 100644 web/src/pages/workflow.tsx diff --git a/web/src/assets/icon/Icon.tsx b/web/src/assets/icon/Icon.tsx index 1d20e3a84..e363af970 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 25e3128bd..a79440874 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 f49bda095..2a81f92f1 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 078be9182..119c1a089 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 f37fe9512..d6ccbdcf3 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 d05bf8389..77af023e0 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 ca4e46cd4..116654d10 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 62dc4db97..a1e5fbca3 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 3f3245a1c..d824d88f1 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 4c23a7835..6aa742c3c 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 9f0ba05e2..35a9560e4 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 7e6fb1e98..dd5263abc 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 4d39821eb..d79d1dc6c 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 e6eb6cc4e..14d7e6077 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 000000000..4a81d7942 --- /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 f9bb28706..4c7ad21f2 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 042c6f30c..342ce40eb 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 5a1bd5142..e55b7ac2e 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 8a6d1ff61..c16286df5 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 6ea265f2a..9b3d76cc4 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 31ef22ac7..3014d9dfc 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 1e38fd930..1f7ce99f1 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 acae1b85e..a44592148 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 37cd376c0..c4b399026 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 bc810c061..0c866e488 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 026453138..b640f08d0 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 00debf47f..cb6651a24 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 52e84567b..8ef5f14d6 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 f532430a6..1fc8cfc26 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 68b611415..53462da31 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 d8541e532..010d5518e 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 0b5a77b1a..500d42fbd 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 e81c79fa8..691dd5d36 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 310cde287..75bd3fb5e 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 ce320b309..4e87fac25 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 f3f2db5e3..521f1c95f 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 000000000..f0f23918a --- /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 94ac5f174..aa9bb169f 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 fa969724b..c4c448c7a 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 df4b7083f..4a92a7f94 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 fea408df9..01b11c220 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 df5595a8e..9e28bf21d 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 44598298c..ce7a3e7d2 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 a517727ba..245e33101 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 9608e3b13..aff2ef38d 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 000000000..a6e5c015b --- /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 000000000..5b8618de8 --- /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 000000000..245ea6abf --- /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 000000000..0a86affaf --- /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 000000000..e69de29bb 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 000000000..e042aca62 --- /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 000000000..6dd68a330 --- /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 000000000..8146db4bc --- /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 82dd856af..50b625683 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 d0b80a24c..3975500f7 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 f82d9c0b6..219dfa0e1 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 063c5ad47..d6cf4593a 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 54fccb9a0..3f645a406 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 a46621095..6585d2408 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 3596c91f1..41b45e99e 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 000000000..975ebb49f --- /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 000000000..f6b609e32 --- /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 456fa0844..801d77a9b 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;