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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 1330 additions and 560 deletions

View File

@ -205,6 +205,36 @@ const QWeatherSvg = () => (
</svg> </svg>
); );
const SemicolonSvg = () => (
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
>
<path
d="M506.88 249.059556a89.884444 89.884444 0 0 0 60.074667-21.845334 87.950222 87.950222 0 0 0 23.210666-61.44 79.189333 79.189333 0 0 0-83.285333-83.285333c-24.576 0-45.056 6.826667-60.074667 23.210667-16.384 15.018667-23.210667 35.498667-23.210666 60.074666 0 24.576 6.826667 45.056 23.210666 61.44 15.018667 13.653333 35.498667 21.845333 60.074667 21.845334zM414.037333 967.224889a262.030222 262.030222 0 0 0 141.994667-88.746667c35.498667-46.421333 53.248-99.669333 53.248-159.744 0-39.594667-9.557333-70.997333-27.306667-95.573333a89.543111 89.543111 0 0 0-75.093333-38.229333c-27.306667 0-47.786667 6.826667-62.805333 23.210666-17.749333 15.018667-25.941333 35.498667-25.941334 61.44 0 23.210667 8.192 43.690667 24.576 60.074667a79.416889 79.416889 0 0 0 58.709334 24.576 78.506667 78.506667 0 0 0 30.037333-5.461333c0 32.768-9.557333 62.805333-30.037333 91.477333a190.008889 190.008889 0 0 1-87.381334 60.074667v66.901333z"
fill={currentColor}
></path>
</svg>
);
const CommaSvg = () => (
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
>
<path
fill={currentColor}
d="M701.312 416.064C701.312 327.68 629.76 256 541.312 256c-88.32 0-160 71.68-160 160.064s71.68 160.064 160 160.064c10.368 0 20.352-1.216 30.144-3.072-27.136 78.592-88.32 99.392-166.4 120.32L434.688 736c228.288-40.576 269.184-268.48 266.688-266.496C707.328 452.672 701.312 434.88 701.312 416.064z"
></path>
</svg>
);
export const ApiIcon = (props: Partial<IconComponentProps>) => ( export const ApiIcon = (props: Partial<IconComponentProps>) => (
<Icon component={ApiSvg} {...props} /> <Icon component={ApiSvg} {...props} />
); );
@ -238,3 +268,11 @@ export const GitHubIcon = (props: Partial<IconComponentProps>) => (
export const QWeatherIcon = (props: Partial<IconComponentProps>) => ( export const QWeatherIcon = (props: Partial<IconComponentProps>) => (
<Icon component={QWeatherSvg} {...props} /> <Icon component={QWeatherSvg} {...props} />
); );
export const SemicolonIcon = (props: Partial<IconComponentProps>) => (
<Icon component={SemicolonSvg} {...props} />
);
export const CommaIcon = (props: Partial<IconComponentProps>) => (
<Icon component={CommaSvg} {...props} />
);

View File

@ -4,16 +4,23 @@ import { useTranslation } from 'react-i18next';
interface IProps { interface IProps {
value?: string | undefined; value?: string | undefined;
onChange?: (val: string | undefined) => void; onChange?: (val: string | undefined) => void;
maxLength?: number;
} }
const DelimiterInput = ({ value, onChange }: IProps) => { export const DelimiterInput = ({ value, onChange, maxLength }: IProps) => {
const nextValue = value?.replaceAll('\n', '\\n'); const nextValue = value?.replaceAll('\n', '\\n');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value; const val = e.target.value;
const nextValue = val.replaceAll('\\n', '\n'); const nextValue = val.replaceAll('\\n', '\n');
onChange?.(nextValue); onChange?.(nextValue);
}; };
return <Input value={nextValue} onChange={handleInputChange}></Input>; return (
<Input
value={nextValue}
onChange={handleInputChange}
maxLength={maxLength}
></Input>
);
}; };
const Delimiter = () => { const Delimiter = () => {

View File

@ -17,6 +17,7 @@ export interface IOperator {
obj: IOperatorNode; obj: IOperatorNode;
downstream: string[]; downstream: string[];
upstream: string[]; upstream: string[];
parent_id?: string;
} }
export interface IOperatorNode { export interface IOperatorNode {

View File

@ -75,3 +75,23 @@
background-color: #eff8ff; background-color: #eff8ff;
border: 1px; border: 1px;
} }
.commonNodeShadow() {
box-shadow:
-6px 0 12px 0 rgba(179, 177, 177, 0.08),
-3px 0 6px -4px rgba(0, 0, 0, 0.12),
-6px 0 16px 6px rgba(0, 0, 0, 0.05);
}
.commonNodeRadius() {
border-radius: 10px;
}
.commonNode() {
.commonNodeShadow();
.commonNodeRadius();
padding: 10px;
background: white;
width: 200px;
}

View File

@ -1077,6 +1077,22 @@ The above is the content you need to summarize.`,
contentTip: 'content: Email content (Optional)', contentTip: 'content: Email content (Optional)',
jsonUploadTypeErrorMessage: 'Please upload json file', jsonUploadTypeErrorMessage: 'Please upload json file',
jsonUploadContentErrorMessage: 'json file error', jsonUploadContentErrorMessage: 'json file error',
iteration: 'Iteration',
iterationDescription: `This component firstly split the input into array by "delimiter".
Perform the same operation steps on the elements in the array in sequence until all results are output, which can be understood as a task batch processor.
For example, within the long text translation iteration node, if all content is input to the LLM node, the single conversation limit may be reached. The upstream node can first split the long text into multiple fragments, and cooperate with the iterative node to perform batch translation on each fragment to avoid reaching the LLM message limit for a single conversation.`,
delimiterTip: `
This delimiter is used to split the input text into several text pieces echo of which will be performed as input item of each iteration.`,
delimiterOptions: {
comma: 'Comma',
lineBreak: 'Line break',
tab: 'Tab',
underline: 'Underline',
diagonal: 'Diagonal',
minus: 'Minus',
semicolon: 'Semicolon',
},
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

View File

@ -1016,6 +1016,20 @@ export default {
templateDescription: '此元件用於排版各種元件的輸出。 ', templateDescription: '此元件用於排版各種元件的輸出。 ',
jsonUploadTypeErrorMessage: '請上傳json檔', jsonUploadTypeErrorMessage: '請上傳json檔',
jsonUploadContentErrorMessage: 'json 檔案錯誤', jsonUploadContentErrorMessage: 'json 檔案錯誤',
iterationDescription: `此元件首先透過「分隔符號」將輸入拆分為陣列。
LLM節點LLM訊息限制`,
delimiterTip: `此分隔符號用於將輸入文字分割成多個文字片段,其中的回顯將作為每次迭代的輸入項執行。`,
delimiterOptions: {
comma: '逗號',
lineBreak: '換行',
tab: '製表符',
underline: '底線',
diagonal: '斜線',
minus: '減號',
semicolon: '分號',
},
}, },
footer: { footer: {
profile: '“保留所有權利 @ react”', profile: '“保留所有權利 @ react”',

View File

@ -1060,6 +1060,20 @@ export default {
contentTip: 'content: 邮件内容(可选)', contentTip: 'content: 邮件内容(可选)',
jsonUploadTypeErrorMessage: '请上传json文件', jsonUploadTypeErrorMessage: '请上传json文件',
jsonUploadContentErrorMessage: 'json 文件错误', jsonUploadContentErrorMessage: 'json 文件错误',
iteration: '循环',
iterationDescription: `该组件首先将输入以“分隔符”分割成数组,然后依次对数组中的元素执行相同的操作步骤,直到输出所有结果,可以理解为一个任务批处理器。
LLM节点LLM消息限制`,
delimiterTip: `该分隔符用于将输入文本分割成几个文本片段,每个文本片段的回显将作为每次迭代的输入项。`,
delimiterOptions: {
comma: '逗号',
lineBreak: '换行',
tab: '制表符',
underline: '下划线',
diagonal: '斜线',
minus: '减号',
semicolon: '分号',
},
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

View File

@ -90,6 +90,7 @@ export function ButtonEdge({
// everything inside EdgeLabelRenderer has no pointer events by default // everything inside EdgeLabelRenderer has no pointer events by default
// if you have an interactive element, set pointer-events: all // if you have an interactive element, set pointer-events: all
pointerEvents: 'all', pointerEvents: 'all',
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
}} }}
className="nodrag nopan" className="nodrag nopan"
> >

View File

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

View File

@ -4,32 +4,24 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { useSetModalState } from '@/hooks/common-hooks';
import { get } from 'lodash';
import { FolderInput, FolderOutput } from 'lucide-react'; import { FolderInput, FolderOutput } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import ReactFlow, { import ReactFlow, {
Background, Background,
ConnectionMode, ConnectionMode,
ControlButton, ControlButton,
Controls, Controls,
NodeMouseHandler,
} from 'reactflow'; } from 'reactflow';
import 'reactflow/dist/style.css'; import 'reactflow/dist/style.css';
import ChatDrawer from '../chat/drawer'; import ChatDrawer from '../chat/drawer';
import { Operator } from '../constant';
import FormDrawer from '../flow-drawer'; import FormDrawer from '../flow-drawer';
import { import {
useGetBeginNodeDataQuery,
useHandleDrop, useHandleDrop,
useHandleExportOrImportJsonFile,
useSelectCanvasData, useSelectCanvasData,
useShowFormDrawer,
useShowSingleDebugDrawer,
useValidateConnection, useValidateConnection,
useWatchNodeFormDataChange, useWatchNodeFormDataChange,
} from '../hooks'; } 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 JsonUploadModal from '../json-upload-modal';
import RunDrawer from '../run-drawer'; import RunDrawer from '../run-drawer';
import { ButtonEdge } from './edge'; import { ButtonEdge } from './edge';
@ -40,6 +32,7 @@ import { CategorizeNode } from './node/categorize-node';
import { EmailNode } from './node/email-node'; import { EmailNode } from './node/email-node';
import { GenerateNode } from './node/generate-node'; import { GenerateNode } from './node/generate-node';
import { InvokeNode } from './node/invoke-node'; import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node'; import { KeywordNode } from './node/keyword-node';
import { LogicNode } from './node/logic-node'; import { LogicNode } from './node/logic-node';
import { MessageNode } from './node/message-node'; import { MessageNode } from './node/message-node';
@ -66,6 +59,8 @@ const nodeTypes = {
invokeNode: InvokeNode, invokeNode: InvokeNode,
templateNode: TemplateNode, templateNode: TemplateNode,
emailNode: EmailNode, emailNode: EmailNode,
group: IterationNode,
iterationStartNode: IterationStartNode,
}; };
const edgeTypes = { const edgeTypes = {
@ -87,66 +82,11 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
onSelectionChange, onSelectionChange,
} = useSelectCanvasData(); } = useSelectCanvasData();
const isValidConnection = useValidateConnection(); 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 controlIconClassname = 'text-black';
const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } =
useShowFormDrawer();
const onPaneClick = useCallback(() => {
hideFormDrawer();
}, [hideFormDrawer]);
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(); 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 { const {
handleExportJson, handleExportJson,
handleImportJson, handleImportJson,
@ -155,25 +95,25 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
hideFileUploadModal, hideFileUploadModal,
} = useHandleExportOrImportJsonFile(); } = useHandleExportOrImportJsonFile();
useEffect(() => { const {
if (drawerVisible) { onNodeClick,
const query: BeginQuery[] = getBeginNodeDataQuery(); onPaneClick,
if (query.length > 0) { clickedNode,
showRunModal(); formDrawerVisible,
hideChatModal(); hideFormDrawer,
} else { singleDebugDrawerVisible,
showChatModal(); hideSingleDebugDrawer,
hideRunModal(); showSingleDebugDrawer,
} chatVisible,
} runVisible,
}, [ hideRunOrChatDrawer,
hideChatModal,
hideRunModal,
showChatModal, showChatModal,
showRunModal, } = useShowDrawer({
drawerVisible, drawerVisible,
getBeginNodeDataQuery, hideDrawer,
]); });
useWatchNodeFormDataChange();
return ( return (
<div className={styles.canvasWrapper}> <div className={styles.canvasWrapper}>
@ -222,6 +162,7 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
strokeWidth: 2, strokeWidth: 2,
stroke: 'rgb(202 197 245)', stroke: 'rgb(202 197 245)',
}, },
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
}} }}
deleteKeyCode={['Delete', 'Backspace']} deleteKeyCode={['Delete', 'Backspace']}
> >

View File

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

View File

@ -3,6 +3,7 @@ import { CopyOutlined } from '@ant-design/icons';
import { Flex, MenuProps } from 'antd'; import { Flex, MenuProps } from 'antd';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Operator } from '../../constant';
import { useDuplicateNode } from '../../hooks'; import { useDuplicateNode } from '../../hooks';
import useGraphStore from '../../store'; import useGraphStore from '../../store';
@ -15,10 +16,17 @@ interface IProps {
const NodeDropdown = ({ id, iconFontColor, label }: IProps) => { const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const deleteNodeById = useGraphStore((store) => store.deleteNodeById); const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
const deleteIterationNodeById = useGraphStore(
(store) => store.deleteIterationNodeById,
);
const deleteNode = useCallback(() => { const deleteNode = useCallback(() => {
deleteNodeById(id); if (label === Operator.Iteration) {
}, [id, deleteNodeById]); deleteIterationNodeById(id);
} else {
deleteNodeById(id);
}
}, [label, deleteIterationNodeById, id, deleteNodeById]);
const duplicateNode = useDuplicateNode(); const duplicateNode = useDuplicateNode();

View File

@ -4,7 +4,7 @@ import { Flex } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { get } from 'lodash'; import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow'; import { Handle, NodeProps, Position } from 'reactflow';
import { useGetComponentLabelByValue } from '../../hooks'; import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
import { IGenerateParameter, NodeData } from '../../interface'; import { IGenerateParameter, NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less'; 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 { .dark {
background: rgb(63, 63, 63) !important; background: rgb(63, 63, 63) !important;
} }
@ -43,6 +31,22 @@
border: 1.5px solid rgb(59, 118, 244); 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 { .handle {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -133,6 +137,12 @@
} }
} }
.iterationNode {
.commonNodeShadow();
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.nodeText { .nodeText {
padding-inline: 0.4em; padding-inline: 0.4em;
padding-block: 0.2em 0.1em; padding-block: 0.2em 0.1em;
@ -142,12 +152,6 @@
.textEllipsis(); .textEllipsis();
} }
.nodeTitle {
font-weight: 600;
text-align: center;
.textEllipsis();
}
.nodeHeader { .nodeHeader {
padding-bottom: 12px; 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 { NextNodePopover } from './popover';
import { RunTooltip } from '../../flow-tooltip'; import { RunTooltip } from '../../flow-tooltip';
import styles from './index.less';
interface IProps { interface IProps {
id: string; id: string;
label: string; label: string;
name: string; name: string;
gap?: number; gap?: number;
className?: string; className?: string;
wrapperClassName?: string;
} }
const ExcludedRunStateOperators = [Operator.Answer];
export function RunStatus({ id, name, label }: IProps) { export function RunStatus({ id, name, label }: IProps) {
const { t } = useTranslate('flow'); const { t } = useTranslate('flow');
return ( 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 ( return (
<section> <section className={wrapperClassName}>
{label !== Operator.Answer && ( {!ExcludedRunStateOperators.includes(label as Operator) && (
<RunStatus id={id} name={name} label={label}></RunStatus> <RunStatus id={id} name={name} label={label}></RunStatus>
)} )}
<Flex <Flex
@ -52,7 +61,9 @@ const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
name={label as Operator} name={label as Operator}
color={operatorMap[label as Operator].color} color={operatorMap[label as Operator].color}
></OperatorIcon> ></OperatorIcon>
<span className={styles.nodeTitle}>{name}</span> <span className="truncate text-center font-semibold text-sm">
{name}
</span>
<NodeDropdown id={id} label={label}></NodeDropdown> <NodeDropdown id={id} label={label}></NodeDropdown>
</Flex> </Flex>
</section> </section>

View File

@ -3,7 +3,7 @@ import get from 'lodash/get';
import React, { MouseEventHandler, useCallback, useMemo } from 'react'; import React, { MouseEventHandler, useCallback, useMemo } from 'react';
import JsonView from 'react18-json-view'; import JsonView from 'react18-json-view';
import 'react18-json-view/src/style.css'; import 'react18-json-view/src/style.css';
import { useGetComponentLabelByValue, useReplaceIdWithText } from '../../hooks'; import { useReplaceIdWithText } from '../../hooks';
import { useTheme } from '@/components/theme-provider'; import { useTheme } from '@/components/theme-provider';
import { import {
@ -20,6 +20,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
interface IProps extends React.PropsWithChildren { interface IProps extends React.PropsWithChildren {
nodeId: string; nodeId: string;

View File

@ -2,7 +2,7 @@ import { useTheme } from '@/components/theme-provider';
import { Divider, Flex } from 'antd'; import { Divider, Flex } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { Handle, NodeProps, Position } from 'reactflow'; import { Handle, NodeProps, Position } from 'reactflow';
import { useGetComponentLabelByValue } from '../../hooks'; import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
import { ISwitchCondition, NodeData } from '../../interface'; import { ISwitchCondition, NodeData } from '../../interface';
import { RightHandleStyle } from './handle-icon'; import { RightHandleStyle } from './handle-icon';
import { useBuildSwitchHandlePositions } from './hooks'; import { useBuildSwitchHandlePositions } from './hooks';

View File

@ -1,13 +1,13 @@
import { useTheme } from '@/components/theme-provider';
import { Flex } from 'antd'; import { Flex } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { get } from 'lodash'; import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow'; import { Handle, NodeProps, Position } from 'reactflow';
import { useGetComponentLabelByValue } from '../../hooks'; import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
import { IGenerateParameter, NodeData } from '../../interface'; import { IGenerateParameter, NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
import { useTheme } from '@/components/theme-provider';
import styles from './index.less'; import styles from './index.less';
export function TemplateNode({ export function TemplateNode({

View File

@ -50,7 +50,9 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import upperFirst from 'lodash/upperFirst'; import upperFirst from 'lodash/upperFirst';
import { import {
CirclePower,
CloudUpload, CloudUpload,
IterationCcw,
ListOrdered, ListOrdered,
OptionIcon, OptionIcon,
TextCursorInput, TextCursorInput,
@ -58,6 +60,8 @@ import {
WrapText, WrapText,
} from 'lucide-react'; } from 'lucide-react';
export const BeginId = 'begin';
export enum Operator { export enum Operator {
Begin = 'Begin', Begin = 'Begin',
Retrieval = 'Retrieval', Retrieval = 'Retrieval',
@ -93,6 +97,8 @@ export enum Operator {
Invoke = 'Invoke', Invoke = 'Invoke',
Template = 'Template', Template = 'Template',
Email = 'Email', Email = 'Email',
Iteration = 'Iteration',
IterationStart = 'IterationItem',
} }
export const CommonOperatorList = Object.values(Operator).filter( export const CommonOperatorList = Object.values(Operator).filter(
@ -134,6 +140,8 @@ export const operatorIconMap = {
[Operator.Invoke]: InvokeIcon, [Operator.Invoke]: InvokeIcon,
[Operator.Template]: TemplateIcon, [Operator.Template]: TemplateIcon,
[Operator.Email]: EmailIcon, [Operator.Email]: EmailIcon,
[Operator.Iteration]: IterationCcw,
[Operator.IterationStart]: CirclePower,
}; };
export const operatorMap: Record< export const operatorMap: Record<
@ -270,6 +278,8 @@ export const operatorMap: Record<
backgroundColor: '#dee0e2', backgroundColor: '#dee0e2',
}, },
[Operator.Email]: { backgroundColor: '#e6f7ff' }, [Operator.Email]: { backgroundColor: '#e6f7ff' },
[Operator.Iteration]: { backgroundColor: '#e6f7ff' },
[Operator.IterationStart]: { backgroundColor: '#e6f7ff' },
}; };
export const componentMenuList = [ export const componentMenuList = [
@ -306,6 +316,9 @@ export const componentMenuList = [
{ {
name: Operator.Template, name: Operator.Template,
}, },
{
name: Operator.Iteration,
},
{ {
name: Operator.Note, name: Operator.Note,
}, },
@ -606,6 +619,11 @@ export const initialEmailValues = {
content: '', content: '',
}; };
export const initialIterationValues = {
delimiter: ',',
};
export const initialIterationStartValues = {};
export const CategorizeAnchorPointPositions = [ export const CategorizeAnchorPointPositions = [
{ top: 1, right: 34 }, { top: 1, right: 34 },
{ top: 8, right: 18 }, { top: 8, right: 18 },
@ -687,6 +705,8 @@ export const RestrictedUpstreamMap = {
[Operator.Invoke]: [Operator.Begin], [Operator.Invoke]: [Operator.Begin],
[Operator.Template]: [Operator.Begin, Operator.Relevant], [Operator.Template]: [Operator.Begin, Operator.Relevant],
[Operator.Email]: [Operator.Begin], [Operator.Email]: [Operator.Begin],
[Operator.Iteration]: [Operator.Begin],
[Operator.IterationStart]: [Operator.Begin],
}; };
export const NodeMap = { export const NodeMap = {
@ -724,6 +744,8 @@ export const NodeMap = {
[Operator.Invoke]: 'invokeNode', [Operator.Invoke]: 'invokeNode',
[Operator.Template]: 'templateNode', [Operator.Template]: 'templateNode',
[Operator.Email]: 'emailNode', [Operator.Email]: 'emailNode',
[Operator.Iteration]: 'group',
[Operator.IterationStart]: 'iterationStartNode',
}; };
export const LanguageOptions = [ export const LanguageOptions = [
@ -2940,4 +2962,5 @@ export const NoDebugOperatorsList = [
Operator.Message, Operator.Message,
Operator.RewriteQuestion, Operator.RewriteQuestion,
Operator.Switch, Operator.Switch,
Operator.Iteration,
]; ];

View File

@ -6,7 +6,7 @@ import { lowerFirst } from 'lodash';
import { Play } from 'lucide-react'; import { Play } from 'lucide-react';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { Node } from 'reactflow'; import { Node } from 'reactflow';
import { Operator, operatorMap } from '../constant'; import { BeginId, Operator, operatorMap } from '../constant';
import AkShareForm from '../form/akshare-form'; import AkShareForm from '../form/akshare-form';
import AnswerForm from '../form/answer-form'; import AnswerForm from '../form/answer-form';
import ArXivForm from '../form/arxiv-form'; import ArXivForm from '../form/arxiv-form';
@ -45,6 +45,7 @@ import { getDrawerWidth, needsSingleStepDebugging } from '../utils';
import SingleDebugDrawer from './single-debug-drawer'; import SingleDebugDrawer from './single-debug-drawer';
import { RunTooltip } from '../flow-tooltip'; import { RunTooltip } from '../flow-tooltip';
import IterationForm from '../form/iteration-from';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
@ -89,6 +90,8 @@ const FormMap = {
[Operator.Note]: () => <></>, [Operator.Note]: () => <></>,
[Operator.Template]: TemplateForm, [Operator.Template]: TemplateForm,
[Operator.Email]: EmailForm, [Operator.Email]: EmailForm,
[Operator.Iteration]: IterationForm,
[Operator.IterationStart]: () => <></>,
}; };
const EmptyContent = () => <div></div>; const EmptyContent = () => <div></div>;
@ -137,11 +140,15 @@ const FormDrawer = ({
<label htmlFor="" className={styles.title}> <label htmlFor="" className={styles.title}>
{t('title')} {t('title')}
</label> </label>
<Input {node?.id === BeginId ? (
value={name} <span>{t(BeginId)}</span>
onBlur={handleNameBlur} ) : (
onChange={handleNameChange} <Input
></Input> value={name}
onBlur={handleNameBlur}
onChange={handleNameChange}
></Input>
)}
</Flex> </Flex>
{needsSingleStepDebugging(operatorName) && ( {needsSingleStepDebugging(operatorName) && (
<RunTooltip> <RunTooltip>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,15 @@
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Collapse, Flex, Form, Input, Select } from 'antd'; import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
import { PropsWithChildren, useCallback } from 'react'; import { PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; 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'; import styles from './index.less';
interface IProps { interface IProps {
nodeId?: string; node?: Node<NodeData>;
} }
enum VariableType { enum VariableType {
@ -18,9 +20,12 @@ enum VariableType {
const getVariableName = (type: string) => const getVariableName = (type: string) =>
type === VariableType.Reference ? 'component_id' : 'value'; type === VariableType.Reference ? 'component_id' : 'value';
const DynamicVariableForm = ({ nodeId }: IProps) => { const DynamicVariableForm = ({ node }: IProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const valueOptions = useBuildComponentIdSelectOptions(nodeId); const valueOptions = useBuildComponentIdSelectOptions(
node?.id,
node?.parentId,
);
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const options = [ const options = [
@ -114,11 +119,11 @@ export function FormCollapse({
); );
} }
const DynamicInputVariable = ({ nodeId }: IProps) => { const DynamicInputVariable = ({ node }: IProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<FormCollapse title={t('flow.input')}> <FormCollapse title={t('flow.input')}>
<DynamicVariableForm nodeId={nodeId}></DynamicVariableForm> <DynamicVariableForm node={node}></DynamicVariableForm>
</FormCollapse> </FormCollapse>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -69,7 +69,7 @@ const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => {
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<DynamicVariablesForm nodeId={node?.id}></DynamicVariablesForm> <DynamicVariablesForm node={node}></DynamicVariablesForm>
</Form> </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} onValuesChange={onValuesChange}
layout={'vertical'} layout={'vertical'}
> >
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable> <DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('type')} name={'type'} initialValue={'flash'}> <Form.Item label={t('type')} name={'type'} initialValue={'flash'}>
<Select options={jin10TypeOptions}></Select> <Select options={jin10TypeOptions}></Select>
</Form.Item> </Form.Item>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,11 +10,14 @@ import { Link, useParams } from 'umi';
import { import {
useGetBeginNodeDataQuery, useGetBeginNodeDataQuery,
useGetBeginNodeDataQueryIsEmpty, useGetBeginNodeDataQueryIsEmpty,
} from '../hooks/use-get-begin-query';
import {
useSaveGraph, useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer, useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange, useWatchAgentChange,
} from '../hooks'; } from '../hooks/use-save-graph';
import { BeginQuery } from '../interface'; import { BeginQuery } from '../interface';
import styles from './index.less'; import styles from './index.less';
interface IProps { 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, { import React, {
ChangeEvent, ChangeEvent,
useCallback, useCallback,
@ -12,23 +8,17 @@ import React, {
import { Connection, Edge, Node, Position, ReactFlowInstance } from 'reactflow'; import { Connection, Edge, Node, Position, ReactFlowInstance } from 'reactflow';
// import { shallow } from 'zustand/shallow'; // import { shallow } from 'zustand/shallow';
import { variableEnabledFieldMap } from '@/constants/chat'; import { variableEnabledFieldMap } from '@/constants/chat';
import { FileMimeType } from '@/constants/common';
import { import {
ModelVariableType, ModelVariableType,
settledModelVariableMap, settledModelVariableMap,
} from '@/constants/knowledge'; } from '@/constants/knowledge';
import { useFetchModelId } from '@/hooks/logic-hooks'; import { useFetchModelId } from '@/hooks/logic-hooks';
import { Variable } from '@/interfaces/database/chat'; import { Variable } from '@/interfaces/database/chat';
import { downloadJsonFile } from '@/utils/file-util'; import { FormInstance, message } from 'antd';
import { useDebounceEffect } from 'ahooks';
import { FormInstance, UploadFile, message } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import dayjs from 'dayjs';
import { humanId } from 'human-id'; import { humanId } from 'human-id';
import { get, isEmpty, lowerFirst, pick } from 'lodash'; import { get, isEmpty, lowerFirst, pick } from 'lodash';
import trim from 'lodash/trim'; import trim from 'lodash/trim';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { import {
NodeMap, NodeMap,
@ -53,6 +43,7 @@ import {
initialGoogleScholarValues, initialGoogleScholarValues,
initialGoogleValues, initialGoogleValues,
initialInvokeValues, initialInvokeValues,
initialIterationValues,
initialJin10Values, initialJin10Values,
initialKeywordExtractValues, initialKeywordExtractValues,
initialMessageValues, initialMessageValues,
@ -69,18 +60,13 @@ import {
initialWikipediaValues, initialWikipediaValues,
initialYahooFinanceValues, initialYahooFinanceValues,
} from './constant'; } from './constant';
import { import { ICategorizeForm, IRelevantForm, ISwitchForm } from './interface';
BeginQuery,
ICategorizeForm,
IRelevantForm,
ISwitchForm,
} from './interface';
import useGraphStore, { RFState } from './store'; import useGraphStore, { RFState } from './store';
import { import {
buildDslComponentsByGraph,
generateNodeNamesWithIncreasingIndex, generateNodeNamesWithIncreasingIndex,
generateSwitchHandleText, generateSwitchHandleText,
getNodeDragHandle, getNodeDragHandle,
getRelativePositionToIterationNode,
replaceIdWithText, replaceIdWithText,
} from './utils'; } from './utils';
@ -145,6 +131,8 @@ export const useInitializeOperatorParams = () => {
[Operator.Invoke]: initialInvokeValues, [Operator.Invoke]: initialInvokeValues,
[Operator.Template]: initialTemplateValues, [Operator.Template]: initialTemplateValues,
[Operator.Email]: initialEmailValues, [Operator.Email]: initialEmailValues,
[Operator.Iteration]: initialIterationValues,
[Operator.IterationStart]: initialIterationValues,
}; };
}, [llmId]); }, [llmId]);
@ -210,7 +198,7 @@ export const useHandleDrop = () => {
x: event.clientX, x: event.clientX,
y: event.clientY, y: event.clientY,
}); });
const newNode = { const newNode: Node<any> = {
id: `${type}:${humanId()}`, id: `${type}:${humanId()}`,
type: NodeMap[type as Operator] || 'ragNode', type: NodeMap[type as Operator] || 'ragNode',
position: position || { position: position || {
@ -227,7 +215,38 @@ export const useHandleDrop = () => {
dragHandle: getNodeDragHandle(type), 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], [reactFlowInstance, getNodeName, nodes, initializeOperatorParams, addNode],
); );
@ -235,78 +254,6 @@ export const useHandleDrop = () => {
return { onDrop, onDragOver, setReactFlowInstance }; 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) => { export const useHandleFormValuesChange = (id?: string) => {
const updateNodeForm = useGraphStore((state) => state.updateNodeForm); const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
const handleValuesChange = useCallback( const handleValuesChange = useCallback(
@ -335,39 +282,6 @@ export const useHandleFormValuesChange = (id?: string) => {
return { handleValuesChange }; 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 = ( export const useSetLlmSetting = (
form?: FormInstance, form?: FormInstance,
formData?: Record<string, any>, formData?: Record<string, any>,
@ -401,7 +315,22 @@ export const useSetLlmSetting = (
}; };
export const useValidateConnection = () => { 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. // restricted lines cannot be connected successfully.
const isValidConnection = useCallback( const isValidConnection = useCallback(
(connection: Connection) => { (connection: Connection) => {
@ -418,10 +347,11 @@ export const useValidateConnection = () => {
!hasLine && !hasLine &&
RestrictedUpstreamMap[ RestrictedUpstreamMap[
getOperatorTypeFromId(connection.source) as Operator getOperatorTypeFromId(connection.source) as Operator
]?.every((x) => x !== getOperatorTypeFromId(connection.target)); ]?.every((x) => x !== getOperatorTypeFromId(connection.target)) &&
isSameNodeChild(connection);
return ret; return ret;
}, },
[edges, getOperatorTypeFromId], [edges, getOperatorTypeFromId, isSameNodeChild],
); );
return isValidConnection; return isValidConnection;
@ -464,52 +394,6 @@ export const useHandleNodeNameChange = ({
return { name, handleNameBlur, handleNameChange }; 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 = () => { export const useReplaceIdWithName = () => {
const getNode = useGraphStore((state) => state.getNode); 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 = () => { export const useDuplicateNode = () => {
const duplicateNodeById = useGraphStore((store) => store.duplicateNode); const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
const getNodeName = useGetNodeName(); const getNodeName = useGetNodeName();
@ -769,107 +593,3 @@ export const useCopyPaste = () => {
}; };
}, [onPasteCapture]); }, [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 FlowCanvas from './canvas';
import Sider from './flow-sider'; import Sider from './flow-sider';
import FlowHeader from './header'; import FlowHeader from './header';
import { useCopyPaste, useFetchDataOnMount } from './hooks'; import { useCopyPaste } from './hooks';
import { useFetchDataOnMount } from './hooks/use-fetch-data';
const { Content } = Layout; const { Content } = Layout;

View File

@ -90,7 +90,7 @@ export interface ISwitchForm {
export type NodeData = { export type NodeData = {
label: string; // operator type label: string; // operator type
name: string; // operator name name: string; // operator name
color: string; color?: string;
form: form:
| IBeginForm | IBeginForm
| IRetrievalForm | IRetrievalForm

View File

@ -4,18 +4,8 @@ import {
useFetchFlowTemplates, useFetchFlowTemplates,
useSetFlow, useSetFlow,
} from '@/hooks/flow-hooks'; } from '@/hooks/flow-hooks';
import { useCallback, useState } from 'react'; import { useCallback } from 'react';
import { useNavigate } from 'umi'; 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 = () => { export const useFetchDataOnMount = () => {
const { data, loading } = useFetchFlowList(); const { data, loading } = useFetchFlowList();
@ -24,7 +14,6 @@ export const useFetchDataOnMount = () => {
}; };
export const useSaveFlow = () => { export const useSaveFlow = () => {
const [currentFlow, setCurrentFlow] = useState({});
const { const {
visible: flowSettingVisible, visible: flowSettingVisible,
hideModal: hideFlowSettingModal, hideModal: hideFlowSettingModal,
@ -39,18 +28,10 @@ export const useSaveFlow = () => {
const templateItem = list.find((x) => x.id === templateId); const templateItem = list.find((x) => x.id === templateId);
let dsl = templateItem?.dsl; let dsl = templateItem?.dsl;
// if (dsl) {
// dsl.graph = headhunter_zh;
// }
const ret = await setFlow({ const ret = await setFlow({
title, title,
dsl, dsl,
avatar: templateItem?.avatar, avatar: templateItem?.avatar,
// dsl: dslJson,
// dsl: {
// ...retrievalRelevantRewriteAndGenerateBase,
// graph: retrievalRelevantRewriteAndGenerate,
// },
}); });
if (ret?.code === 0) { if (ret?.code === 0) {
@ -61,20 +42,12 @@ export const useSaveFlow = () => {
[setFlow, hideFlowSettingModal, navigate, list], [setFlow, hideFlowSettingModal, navigate, list],
); );
const handleShowFlowSettingModal = useCallback(
async (record: any) => {
setCurrentFlow(record);
showFileRenameModal();
},
[showFileRenameModal],
);
return { return {
flowSettingLoading: loading, flowSettingLoading: loading,
initialFlowName: '', initialFlowName: '',
onFlowOk, onFlowOk,
flowSettingVisible, flowSettingVisible,
hideFlowSettingModal, hideFlowSettingModal,
showFlowSettingModal: handleShowFlowSettingModal, showFlowSettingModal: showFileRenameModal,
}; };
}; };

View File

@ -2,16 +2,14 @@ import { IModalProps } from '@/interfaces/common';
import { Drawer } from 'antd'; import { Drawer } from 'antd';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import { BeginId } from '../constant';
useGetBeginNodeDataQuery, import DebugContent from '../debug-content';
useSaveGraphBeforeOpeningDebugDrawer, import { useGetBeginNodeDataQuery } from '../hooks/use-get-begin-query';
} from '../hooks'; import { useSaveGraphBeforeOpeningDebugDrawer } from '../hooks/use-save-graph';
import { BeginQuery } from '../interface'; import { BeginQuery } from '../interface';
import useGraphStore from '../store'; import useGraphStore from '../store';
import { getDrawerWidth } from '../utils'; import { getDrawerWidth } from '../utils';
import DebugContent from '../debug-content';
const RunDrawer = ({ const RunDrawer = ({
hideModal, hideModal,
showModal: showChatModal, showModal: showChatModal,
@ -28,7 +26,7 @@ const RunDrawer = ({
const handleRunAgent = useCallback( const handleRunAgent = useCallback(
(nextValues: Record<string, any>) => { (nextValues: Record<string, any>) => {
const currentNodes = updateNodeForm('begin', nextValues, ['query']); const currentNodes = updateNodeForm(BeginId, nextValues, ['query']);
handleRun(currentNodes); handleRun(currentNodes);
hideModal?.(); hideModal?.();
}, },

View File

@ -1,5 +1,5 @@
import type {} from '@redux-devtools/extension'; import type {} from '@redux-devtools/extension';
import { humanId } from 'human-id'; import { omit } from 'lodash';
import differenceWith from 'lodash/differenceWith'; import differenceWith from 'lodash/differenceWith';
import intersectionWith from 'lodash/intersectionWith'; import intersectionWith from 'lodash/intersectionWith';
import lodashSet from 'lodash/set'; import lodashSet from 'lodash/set';
@ -25,8 +25,8 @@ import { Operator, SwitchElseTo } from './constant';
import { NodeData } from './interface'; import { NodeData } from './interface';
import { import {
duplicateNodeForm, duplicateNodeForm,
generateDuplicateNode,
generateNodeNamesWithIncreasingIndex, generateNodeNamesWithIncreasingIndex,
getNodeDragHandle,
getOperatorIndex, getOperatorIndex,
isEdgeEqual, isEdgeEqual,
} from './utils'; } from './utils';
@ -61,13 +61,16 @@ export type RFState = {
) => void; ) => void;
deletePreviousEdgeOfClassificationNode: (connection: Connection) => void; deletePreviousEdgeOfClassificationNode: (connection: Connection) => void;
duplicateNode: (id: string, name: string) => void; duplicateNode: (id: string, name: string) => void;
duplicateIterationNode: (id: string, name: string) => void;
deleteEdge: () => void; deleteEdge: () => void;
deleteEdgeById: (id: string) => void; deleteEdgeById: (id: string) => void;
deleteNodeById: (id: string) => void; deleteNodeById: (id: string) => void;
deleteIterationNodeById: (id: string) => void;
deleteEdgeBySourceAndSourceHandle: (connection: Partial<Connection>) => void; deleteEdgeBySourceAndSourceHandle: (connection: Partial<Connection>) => void;
findNodeByName: (operatorName: Operator) => Node | undefined; findNodeByName: (operatorName: Operator) => Node | undefined;
updateMutableNodeFormItem: (id: string, field: string, value: any) => void; updateMutableNodeFormItem: (id: string, field: string, value: any) => void;
getOperatorTypeFromId: (id?: string | null) => string | undefined; getOperatorTypeFromId: (id?: string | null) => string | undefined;
getParentIdById: (id?: string | null) => string | undefined;
updateNodeName: (id: string, name: string) => void; updateNodeName: (id: string, name: string) => void;
generateNodeName: (name: string) => string; generateNodeName: (name: string) => string;
setClickedNodeId: (id?: string) => void; setClickedNodeId: (id?: string) => void;
@ -170,6 +173,9 @@ const useGraphStore = create<RFState>()(
getOperatorTypeFromId: (id?: string | null) => { getOperatorTypeFromId: (id?: string | null) => {
return get().getNode(id)?.data?.label; return get().getNode(id)?.data?.label;
}, },
getParentIdById: (id?: string | null) => {
return get().getNode(id)?.parentId;
},
addEdge: (connection: Connection) => { addEdge: (connection: Connection) => {
set({ set({
edges: addEdge(connection, get().edges), edges: addEdge(connection, get().edges),
@ -234,12 +240,14 @@ const useGraphStore = create<RFState>()(
} }
}, },
duplicateNode: (id: string, name: string) => { duplicateNode: (id: string, name: string) => {
const { getNode, addNode, generateNodeName } = get(); const { getNode, addNode, generateNodeName, duplicateIterationNode } =
get();
const node = getNode(id); const node = getNode(id);
const position = {
x: (node?.position?.x || 0) + 50, if (node?.data.label === Operator.Iteration) {
y: (node?.position?.y || 0) + 50, duplicateIterationNode(id, name);
}; return;
}
addNode({ addNode({
...(node || {}), ...(node || {}),
@ -247,13 +255,38 @@ const useGraphStore = create<RFState>()(
...duplicateNodeForm(node?.data), ...duplicateNodeForm(node?.data),
name: generateNodeName(name), name: generateNodeName(name),
}, },
selected: false, ...generateDuplicateNode(node?.position, node?.data?.label),
dragging: false,
id: `${node?.data?.label}:${humanId()}`,
position,
dragHandle: getNodeDragHandle(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: () => { deleteEdge: () => {
const { edges, selectedEdgeIds } = get(); const { edges, selectedEdgeIds } = get();
set({ set({
@ -323,6 +356,21 @@ const useGraphStore = create<RFState>()(
.filter((edge) => edge.target !== id), .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) => { findNodeByName: (name: Operator) => {
return get().nodes.find((x) => x.data.label === name); 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 { curry, get, intersectionWith, isEqual, sample } from 'lodash';
import pipe from 'lodash/fp/pipe'; import pipe from 'lodash/fp/pipe';
import isObject from 'lodash/isObject'; 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 { v4 as uuidv4 } from 'uuid';
import { import {
CategorizeAnchorPointPositions, CategorizeAnchorPointPositions,
@ -144,6 +144,7 @@ export const buildDslComponentsByGraph = (
}, },
downstream: buildComponentDownstreamOrUpstream(edges, id, true), downstream: buildComponentDownstreamOrUpstream(edges, id, true),
upstream: buildComponentDownstreamOrUpstream(edges, id, false), upstream: buildComponentDownstreamOrUpstream(edges, id, false),
parent_id: x?.parentId,
}; };
}); });
@ -332,3 +333,55 @@ export const getDrawerWidth = () => {
export const needsSingleStepDebugging = (label: string) => { export const needsSingleStepDebugging = (label: string) => {
return !NoDebugOperatorsList.some((x) => (label as Operator) === x); 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, handleInputChange,
loading, loading,
} = useInfiniteFetchKnowledgeList(); } = useInfiniteFetchKnowledgeList();
console.log('🚀 ~ KnowledgeList ~ data:', data);
const nextList = data?.pages?.flatMap((x) => x.kbs) ?? []; const nextList = data?.pages?.flatMap((x) => x.kbs) ?? [];
const total = useMemo(() => { 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;

View File

@ -246,6 +246,11 @@ const routes = [
}, },
], ],
}, },
{
path: '/workflow',
component: '@/pages/workflow',
layout: false,
},
]; ];
export default routes; export default routes;