@@ -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 (
+
+ );
+}
+
+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;