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)
This commit is contained in:
balibabu
2024-12-27 11:24:17 +08:00
committed by GitHub
parent a6f4153775
commit a1a825c830
72 changed files with 1330 additions and 560 deletions

View File

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

View File

@ -1,4 +1,10 @@
.canvasWrapper {
position: relative;
height: 100%;
:global(.react-flow__node-group) {
.commonNode();
padding: 0;
border: 0;
background-color: transparent;
}
}

View File

@ -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 (
<div className={styles.canvasWrapper}>
@ -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']}
>

View File

@ -44,7 +44,9 @@ export function BeginNode({ selected, data }: NodeProps<NodeData>) {
fontSize={24}
color={operatorMap[data.label as Operator].color}
></OperatorIcon>
<div className={styles.nodeTitle}>{t(`flow.begin`)}</div>
<div className="truncate text-center font-semibold text-sm">
{t(`flow.begin`)}
</div>
</Flex>
<Flex gap={8} vertical className={styles.generateParameters}>
{query.map((x, idx) => {

View File

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

View File

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

View File

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

View File

@ -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 (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="#5025f9"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={{ position: 'absolute', right: 5, bottom: 5 }}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="16 20 20 20 20 16" />
<line x1="14" y1="14" x2="20" y2="20" />
<polyline points="8 4 4 4 4 8" />
<line x1="4" y1="4" x2="10" y2="10" />
</svg>
);
}
const controlStyle = {
background: 'transparent',
border: 'none',
};
export function IterationNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const { theme } = useTheme();
return (
<section
className={cn(
'w-full h-full bg-zinc-200 opacity-70',
styles.iterationNode,
{
['bg-gray-800']: theme === 'dark',
[styles.selectedIterationNode]: selected,
},
)}
>
<NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}>
<ResizeIcon />
</NodeResizeControl>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
id="b"
style={RightHandleStyle}
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
wrapperClassName={cn(
'p-2 bg-white rounded-t-[10px] absolute w-full top-[-60px] left-[-0.3px]',
styles.iterationHeader,
{
[`${styles.dark} text-white`]: theme === 'dark',
[styles.selectedHeader]: selected,
},
)}
></NodeHeader>
</section>
);
}
export function IterationStartNode({
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const { theme } = useTheme();
return (
<section
className={cn('bg-white p-2 rounded-xl', {
[styles.dark]: theme === 'dark',
[styles.selectedNode]: selected,
})}
>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
></Handle>
<div>
<ListRestart className="size-7" />
</div>
</section>
);
}

View File

@ -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 (
<section>
{label !== Operator.Answer && (
<section className={wrapperClassName}>
{!ExcludedRunStateOperators.includes(label as Operator) && (
<RunStatus id={id} name={name} label={label}></RunStatus>
)}
<Flex
@ -52,7 +61,9 @@ const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
name={label as Operator}
color={operatorMap[label as Operator].color}
></OperatorIcon>
<span className={styles.nodeTitle}>{name}</span>
<span className="truncate text-center font-semibold text-sm">
{name}
</span>
<NodeDropdown id={id} label={label}></NodeDropdown>
</Flex>
</section>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = () => <div></div>;
@ -137,11 +140,15 @@ const FormDrawer = ({
<label htmlFor="" className={styles.title}>
{t('title')}
</label>
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
{node?.id === BeginId ? (
<span>{t(BeginId)}</span>
) : (
<Input
value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
)}
</Flex>
{needsSingleStepDebugging(operatorName) && (
<RunTooltip>

View File

@ -12,7 +12,7 @@ const AkShareForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10} max={99}></TopNItem>
</Form>
);

View File

@ -23,7 +23,7 @@ const ArXivForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item label={t('sortBy')} name={'sort_by'}>

View File

@ -39,7 +39,7 @@ const BaiduFanyiForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('appid')} name={'appid'}>
<Input></Input>
</Form.Item>

View File

@ -12,7 +12,7 @@ const BaiduForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
</Form>
);

View File

@ -21,7 +21,7 @@ const BingForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item label={t('channel')} name={'channel'}>
<Select options={options}></Select>

View File

@ -24,7 +24,7 @@ const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => {
initialValues={{ items: [{}] }}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}

View File

@ -1,13 +1,15 @@
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
import { PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useBuildComponentIdSelectOptions } from '../../hooks';
import { Node } from 'reactflow';
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
import { NodeData } from '../../interface';
import styles from './index.less';
interface IProps {
nodeId?: string;
node?: Node<NodeData>;
}
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 (
<FormCollapse title={t('flow.input')}>
<DynamicVariableForm nodeId={nodeId}></DynamicVariableForm>
<DynamicVariableForm node={node}></DynamicVariableForm>
</FormCollapse>
);
};

View File

@ -20,7 +20,7 @@ const CrawlerForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('proxy')} name={'proxy'}>
<Input placeholder="like: http://127.0.0.1:8888"></Input>
</Form.Item>

View File

@ -18,7 +18,7 @@ const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={5}></TopNItem>
<Form.Item label={t('authKey')} name={'auth_key'}>
<Select options={options}></Select>

View File

@ -21,7 +21,7 @@ const DuckDuckGoForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item
label={t('channel')}

View File

@ -14,7 +14,7 @@ const EmailForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
{/* SMTP服务器配置 */}
<Form.Item label={t('smtpServer')} name={'smtp_server'}>

View File

@ -24,7 +24,7 @@ const ExeSQLForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
label={t('dbType')}
name={'db_type'}

View File

@ -2,14 +2,14 @@ import { EditableCell, EditableRow } from '@/components/editable-cell';
import { useTranslate } from '@/hooks/common-hooks';
import { DeleteOutlined } from '@ant-design/icons';
import { Button, Flex, Select, Table, TableProps } from 'antd';
import { IGenerateParameter } from '../../interface';
import { useBuildComponentIdSelectOptions } from '../../hooks';
import { Node } from 'reactflow';
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
import { IGenerateParameter, NodeData } from '../../interface';
import { useHandleOperateParameters } from './hooks';
import styles from './index.less';
import styles from './index.less';
interface IProps {
nodeId?: string;
node?: Node<NodeData>;
}
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,

View File

@ -49,7 +49,7 @@ const GenerateForm = ({ onValuesChange, form, node }: IOperatorForm) => {
<MessageHistoryWindowSizeItem
initialValue={12}
></MessageHistoryWindowSizeItem>
<DynamicParameters nodeId={node?.id}></DynamicParameters>
<DynamicParameters node={node}></DynamicParameters>
</Form>
);
};

View File

@ -12,7 +12,7 @@ const GithubForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={5}></TopNItem>
</Form>
);

View File

@ -16,7 +16,7 @@ const GoogleForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item label={t('apiKey')} name={'api_key'}>
<Input></Input>

View File

@ -45,7 +45,7 @@ const GoogleScholarForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={5}></TopNItem>
<Form.Item
label={t('sortBy')}

View File

@ -2,15 +2,16 @@ import { EditableCell, EditableRow } from '@/components/editable-cell';
import { useTranslate } from '@/hooks/common-hooks';
import { DeleteOutlined } from '@ant-design/icons';
import { Button, Collapse, Flex, Input, Select, Table, TableProps } from 'antd';
import { useBuildComponentIdSelectOptions } from '../../hooks';
import { IInvokeVariable } from '../../interface';
import { trim } from 'lodash';
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
import { IInvokeVariable, NodeData } from '../../interface';
import { useHandleOperateParameters } from './hooks';
import { trim } from 'lodash';
import { Node } from 'reactflow';
import styles from './index.less';
interface IProps {
nodeId?: string;
node?: Node<NodeData>;
}
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,

View File

@ -69,7 +69,7 @@ const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => {
>
<Switch />
</Form.Item>
<DynamicVariablesForm nodeId={node?.id}></DynamicVariablesForm>
<DynamicVariablesForm node={node}></DynamicVariablesForm>
</Form>
</>
);

View File

@ -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: (
<div className="flex items-center gap-2">
<Icon className={'size-4'}></Icon>
{t(`flow.delimiterOptions.${x.text}`)}
</div>
),
};
});
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={['delimiter']}
label={t('knowledgeDetails.delimiter')}
initialValue={`\\n!?;。;!?`}
rules={[{ required: true }]}
tooltip={t('flow.delimiterTip')}
>
<Select options={options}></Select>
</Form.Item>
</Form>
);
};
export default IterationForm;

View File

@ -65,7 +65,7 @@ const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('type')} name={'type'} initialValue={'flash'}>
<Select options={jin10TypeOptions}></Select>
</Form.Item>

View File

@ -16,7 +16,7 @@ const KeywordExtractForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}

View File

@ -15,7 +15,7 @@ const PubMedForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item
label={t('email')}

View File

@ -55,7 +55,7 @@ const QWeatherForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('webApiKey')} name={'web_apikey'}>
<Input></Input>
</Form.Item>

View File

@ -32,7 +32,7 @@ const RetrievalForm = ({ onValuesChange, form, node }: IOperatorForm) => {
form={form}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<SimilaritySlider
isTooltipShown
vectorSimilarityWeightName="keywords_similarity_weight"

View File

@ -9,7 +9,7 @@ import {
SwitchOperatorOptions,
} from '../../constant';
import { useBuildFormSelectOptions } from '../../form-hooks';
import { useBuildComponentIdSelectOptions } from '../../hooks';
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
import { IOperatorForm, ISwitchForm } from '../../interface';
import { getOtherFieldValues } from '../../utils';
@ -43,7 +43,10 @@ const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => {
}));
}, [t]);
const componentIdOptions = useBuildComponentIdSelectOptions(node?.id);
const componentIdOptions = useBuildComponentIdSelectOptions(
node?.id,
node?.parentId,
);
return (
<Form

View File

@ -18,7 +18,7 @@ const TemplateForm = ({ onValuesChange, form, node }: IOperatorForm) => {
<Input.TextArea rows={8} placeholder={t('flow.blank')} />
</Form.Item>
<DynamicParameters nodeId={node?.id}></DynamicParameters>
<DynamicParameters node={node}></DynamicParameters>
</Form>
);
};

View File

@ -56,7 +56,7 @@ const TuShareForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
label={t('token')}
name={'token'}

View File

@ -24,7 +24,7 @@ const WenCaiForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={20} max={99}></TopNItem>
<Form.Item label={t('queryType')} name={'query_type'}>
<Select options={wenCaiQueryTypeOptions}></Select>

View File

@ -16,7 +16,7 @@ const WikipediaForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={10}></TopNItem>
<Form.Item label={t('language')} name={'language'}>
<Select options={LanguageOptions}></Select>

View File

@ -14,7 +14,7 @@ const YahooFinanceForm = ({ onValuesChange, form, node }: IOperatorForm) => {
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('info')} name={'info'}>
<Switch></Switch>
</Form.Item>

View File

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

View File

@ -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<any> = {
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<any> = {
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<string, any>,
@ -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: <span>Component Output</span>,
title: 'Component Output',
options: componentIdOptions,
},
{
label: <span>Begin Input</span>,
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<DefaultOptionType[]>((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<string>();
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,
};
};

View File

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

View File

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

View File

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

View File

@ -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<NodeData>) => {
// 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: <span>Component Output</span>,
title: 'Component Output',
options: componentIdOptions,
},
{
label: <span>Begin Input</span>,
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<DefaultOptionType[]>((pre, cur) => {
return [...pre, ...cur.options];
}, []),
[options],
);
const getLabel = useCallback(
(val?: string) => {
return flattenOptions.find((x) => x.value === val)?.label;
},
[flattenOptions],
);
return getLabel;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string, any>) => {
const currentNodes = updateNodeForm('begin', nextValues, ['query']);
const currentNodes = updateNodeForm(BeginId, nextValues, ['query']);
handleRun(currentNodes);
hideModal?.();
},

View File

@ -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<Connection>) => 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<RFState>()(
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<RFState>()(
}
},
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<RFState>()(
...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<NodeData> = {
...(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<RFState>()(
.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);
},

View File

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

View File

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

View File

@ -0,0 +1,5 @@
.react-flow-subflows-example {
.react-flow__node-group {
padding: 0;
}
}

151
web/src/pages/workflow.tsx Normal file
View File

@ -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<any>) {
return (
<section className="ragflow-group w-full h-full">
<div className="h-10 bg-slate-200 text-orange-400">header</div>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
id="b"
></Handle>
<div className="w-full h-10">xxx</div>
</section>
);
}
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 (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
className="react-flow-subflows-example"
fitView
onNodeClick={(node) => {
console.log(node);
}}
nodeTypes={nodeTypes}
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
};
export default NestedFlow;