mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-08-14 03:35:52 +08:00
### 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:
parent
a6f4153775
commit
a1a825c830
@ -205,6 +205,36 @@ const QWeatherSvg = () => (
|
||||
</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>) => (
|
||||
<Icon component={ApiSvg} {...props} />
|
||||
);
|
||||
@ -238,3 +268,11 @@ export const GitHubIcon = (props: Partial<IconComponentProps>) => (
|
||||
export const QWeatherIcon = (props: Partial<IconComponentProps>) => (
|
||||
<Icon component={QWeatherSvg} {...props} />
|
||||
);
|
||||
|
||||
export const SemicolonIcon = (props: Partial<IconComponentProps>) => (
|
||||
<Icon component={SemicolonSvg} {...props} />
|
||||
);
|
||||
|
||||
export const CommaIcon = (props: Partial<IconComponentProps>) => (
|
||||
<Icon component={CommaSvg} {...props} />
|
||||
);
|
||||
|
@ -4,16 +4,23 @@ import { useTranslation } from 'react-i18next';
|
||||
interface IProps {
|
||||
value?: string | undefined;
|
||||
onChange?: (val: string | undefined) => void;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
const DelimiterInput = ({ value, onChange }: IProps) => {
|
||||
export const DelimiterInput = ({ value, onChange, maxLength }: IProps) => {
|
||||
const nextValue = value?.replaceAll('\n', '\\n');
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
const nextValue = val.replaceAll('\\n', '\n');
|
||||
onChange?.(nextValue);
|
||||
};
|
||||
return <Input value={nextValue} onChange={handleInputChange}></Input>;
|
||||
return (
|
||||
<Input
|
||||
value={nextValue}
|
||||
onChange={handleInputChange}
|
||||
maxLength={maxLength}
|
||||
></Input>
|
||||
);
|
||||
};
|
||||
|
||||
const Delimiter = () => {
|
||||
|
@ -17,6 +17,7 @@ export interface IOperator {
|
||||
obj: IOperatorNode;
|
||||
downstream: string[];
|
||||
upstream: string[];
|
||||
parent_id?: string;
|
||||
}
|
||||
|
||||
export interface IOperatorNode {
|
||||
|
@ -75,3 +75,23 @@
|
||||
background-color: #eff8ff;
|
||||
border: 1px;
|
||||
}
|
||||
|
||||
.commonNodeShadow() {
|
||||
box-shadow:
|
||||
-6px 0 12px 0 rgba(179, 177, 177, 0.08),
|
||||
-3px 0 6px -4px rgba(0, 0, 0, 0.12),
|
||||
-6px 0 16px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.commonNodeRadius() {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.commonNode() {
|
||||
.commonNodeShadow();
|
||||
.commonNodeRadius();
|
||||
|
||||
padding: 10px;
|
||||
background: white;
|
||||
width: 200px;
|
||||
}
|
||||
|
@ -1077,6 +1077,22 @@ The above is the content you need to summarize.`,
|
||||
contentTip: 'content: Email content (Optional)',
|
||||
jsonUploadTypeErrorMessage: 'Please upload json file',
|
||||
jsonUploadContentErrorMessage: 'json file error',
|
||||
iteration: 'Iteration',
|
||||
iterationDescription: `This component firstly split the input into array by "delimiter".
|
||||
Perform the same operation steps on the elements in the array in sequence until all results are output, which can be understood as a task batch processor.
|
||||
|
||||
For example, within the long text translation iteration node, if all content is input to the LLM node, the single conversation limit may be reached. The upstream node can first split the long text into multiple fragments, and cooperate with the iterative node to perform batch translation on each fragment to avoid reaching the LLM message limit for a single conversation.`,
|
||||
delimiterTip: `
|
||||
This delimiter is used to split the input text into several text pieces echo of which will be performed as input item of each iteration.`,
|
||||
delimiterOptions: {
|
||||
comma: 'Comma',
|
||||
lineBreak: 'Line break',
|
||||
tab: 'Tab',
|
||||
underline: 'Underline',
|
||||
diagonal: 'Diagonal',
|
||||
minus: 'Minus',
|
||||
semicolon: 'Semicolon',
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
profile: 'All rights reserved @ React',
|
||||
|
@ -1016,6 +1016,20 @@ export default {
|
||||
templateDescription: '此元件用於排版各種元件的輸出。 ',
|
||||
jsonUploadTypeErrorMessage: '請上傳json檔',
|
||||
jsonUploadContentErrorMessage: 'json 檔案錯誤',
|
||||
iterationDescription: `此元件首先透過「分隔符號」將輸入拆分為陣列。
|
||||
對數組中的元素依序執行相同的操作步驟,直到輸出所有結果,可以理解為任務批次處理器。
|
||||
|
||||
例如,在長文本翻譯迭代節點內,如果所有內容都輸入到LLM節點,則可能會達到單次對話限制。上游節點可以先將長文本拆分為多個分片,並配合迭代節點對每個分片進行批次翻譯,避免達到單次對話的LLM訊息限制。`,
|
||||
delimiterTip: `此分隔符號用於將輸入文字分割成多個文字片段,其中的回顯將作為每次迭代的輸入項執行。`,
|
||||
delimiterOptions: {
|
||||
comma: '逗號',
|
||||
lineBreak: '換行',
|
||||
tab: '製表符',
|
||||
underline: '底線',
|
||||
diagonal: '斜線',
|
||||
minus: '減號',
|
||||
semicolon: '分號',
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
profile: '“保留所有權利 @ react”',
|
||||
|
@ -1060,6 +1060,20 @@ export default {
|
||||
contentTip: 'content: 邮件内容(可选)',
|
||||
jsonUploadTypeErrorMessage: '请上传json文件',
|
||||
jsonUploadContentErrorMessage: 'json 文件错误',
|
||||
iteration: '循环',
|
||||
iterationDescription: `该组件首先将输入以“分隔符”分割成数组,然后依次对数组中的元素执行相同的操作步骤,直到输出所有结果,可以理解为一个任务批处理器。
|
||||
|
||||
例如在长文本翻译迭代节点中,如果所有内容都输入到LLM节点,可能会达到单次对话的限制,上游节点可以先将长文本分割成多个片段,配合迭代节点对每个片段进行批量翻译,避免达到单次对话的LLM消息限制。`,
|
||||
delimiterTip: `该分隔符用于将输入文本分割成几个文本片段,每个文本片段的回显将作为每次迭代的输入项。`,
|
||||
delimiterOptions: {
|
||||
comma: '逗号',
|
||||
lineBreak: '换行',
|
||||
tab: '制表符',
|
||||
underline: '下划线',
|
||||
diagonal: '斜线',
|
||||
minus: '减号',
|
||||
semicolon: '分号',
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
profile: 'All rights reserved @ React',
|
||||
|
@ -90,6 +90,7 @@ export function ButtonEdge({
|
||||
// everything inside EdgeLabelRenderer has no pointer events by default
|
||||
// if you have an interactive element, set pointer-events: all
|
||||
pointerEvents: 'all',
|
||||
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
|
||||
}}
|
||||
className="nodrag nopan"
|
||||
>
|
||||
|
@ -1,4 +1,10 @@
|
||||
.canvasWrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
:global(.react-flow__node-group) {
|
||||
.commonNode();
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
@ -4,32 +4,24 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { get } from 'lodash';
|
||||
import { FolderInput, FolderOutput } from 'lucide-react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
ControlButton,
|
||||
Controls,
|
||||
NodeMouseHandler,
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import ChatDrawer from '../chat/drawer';
|
||||
import { Operator } from '../constant';
|
||||
import FormDrawer from '../flow-drawer';
|
||||
import {
|
||||
useGetBeginNodeDataQuery,
|
||||
useHandleDrop,
|
||||
useHandleExportOrImportJsonFile,
|
||||
useSelectCanvasData,
|
||||
useShowFormDrawer,
|
||||
useShowSingleDebugDrawer,
|
||||
useValidateConnection,
|
||||
useWatchNodeFormDataChange,
|
||||
} from '../hooks';
|
||||
import { BeginQuery } from '../interface';
|
||||
import { useHandleExportOrImportJsonFile } from '../hooks/use-export-json';
|
||||
import { useShowDrawer } from '../hooks/use-show-drawer';
|
||||
import JsonUploadModal from '../json-upload-modal';
|
||||
import RunDrawer from '../run-drawer';
|
||||
import { ButtonEdge } from './edge';
|
||||
@ -40,6 +32,7 @@ import { CategorizeNode } from './node/categorize-node';
|
||||
import { EmailNode } from './node/email-node';
|
||||
import { GenerateNode } from './node/generate-node';
|
||||
import { InvokeNode } from './node/invoke-node';
|
||||
import { IterationNode, IterationStartNode } from './node/iteration-node';
|
||||
import { KeywordNode } from './node/keyword-node';
|
||||
import { LogicNode } from './node/logic-node';
|
||||
import { MessageNode } from './node/message-node';
|
||||
@ -66,6 +59,8 @@ const nodeTypes = {
|
||||
invokeNode: InvokeNode,
|
||||
templateNode: TemplateNode,
|
||||
emailNode: EmailNode,
|
||||
group: IterationNode,
|
||||
iterationStartNode: IterationStartNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
@ -87,66 +82,11 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
onSelectionChange,
|
||||
} = useSelectCanvasData();
|
||||
const isValidConnection = useValidateConnection();
|
||||
const {
|
||||
visible: runVisible,
|
||||
showModal: showRunModal,
|
||||
hideModal: hideRunModal,
|
||||
} = useSetModalState();
|
||||
const {
|
||||
visible: chatVisible,
|
||||
showModal: showChatModal,
|
||||
hideModal: hideChatModal,
|
||||
} = useSetModalState();
|
||||
const {
|
||||
singleDebugDrawerVisible,
|
||||
showSingleDebugDrawer,
|
||||
hideSingleDebugDrawer,
|
||||
} = useShowSingleDebugDrawer();
|
||||
|
||||
const controlIconClassname = 'text-black';
|
||||
|
||||
const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } =
|
||||
useShowFormDrawer();
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
hideFormDrawer();
|
||||
}, [hideFormDrawer]);
|
||||
|
||||
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
|
||||
|
||||
useWatchNodeFormDataChange();
|
||||
|
||||
const hideRunOrChatDrawer = useCallback(() => {
|
||||
hideChatModal();
|
||||
hideRunModal();
|
||||
hideDrawer();
|
||||
}, [hideChatModal, hideDrawer, hideRunModal]);
|
||||
|
||||
const onNodeClick: NodeMouseHandler = useCallback(
|
||||
(e, node) => {
|
||||
if (node.data.label !== Operator.Note) {
|
||||
hideSingleDebugDrawer();
|
||||
hideRunOrChatDrawer();
|
||||
showFormDrawer(node);
|
||||
}
|
||||
// handle single debug icon click
|
||||
if (
|
||||
get(e.target, 'dataset.play') === 'true' ||
|
||||
get(e.target, 'parentNode.dataset.play') === 'true'
|
||||
) {
|
||||
showSingleDebugDrawer();
|
||||
}
|
||||
},
|
||||
[
|
||||
hideRunOrChatDrawer,
|
||||
hideSingleDebugDrawer,
|
||||
showFormDrawer,
|
||||
showSingleDebugDrawer,
|
||||
],
|
||||
);
|
||||
|
||||
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
|
||||
|
||||
const {
|
||||
handleExportJson,
|
||||
handleImportJson,
|
||||
@ -155,25 +95,25 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
hideFileUploadModal,
|
||||
} = useHandleExportOrImportJsonFile();
|
||||
|
||||
useEffect(() => {
|
||||
if (drawerVisible) {
|
||||
const query: BeginQuery[] = getBeginNodeDataQuery();
|
||||
if (query.length > 0) {
|
||||
showRunModal();
|
||||
hideChatModal();
|
||||
} else {
|
||||
showChatModal();
|
||||
hideRunModal();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
hideChatModal,
|
||||
hideRunModal,
|
||||
const {
|
||||
onNodeClick,
|
||||
onPaneClick,
|
||||
clickedNode,
|
||||
formDrawerVisible,
|
||||
hideFormDrawer,
|
||||
singleDebugDrawerVisible,
|
||||
hideSingleDebugDrawer,
|
||||
showSingleDebugDrawer,
|
||||
chatVisible,
|
||||
runVisible,
|
||||
hideRunOrChatDrawer,
|
||||
showChatModal,
|
||||
showRunModal,
|
||||
} = useShowDrawer({
|
||||
drawerVisible,
|
||||
getBeginNodeDataQuery,
|
||||
]);
|
||||
hideDrawer,
|
||||
});
|
||||
|
||||
useWatchNodeFormDataChange();
|
||||
|
||||
return (
|
||||
<div className={styles.canvasWrapper}>
|
||||
@ -222,6 +162,7 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
strokeWidth: 2,
|
||||
stroke: 'rgb(202 197 245)',
|
||||
},
|
||||
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
|
||||
}}
|
||||
deleteKeyCode={['Delete', 'Backspace']}
|
||||
>
|
||||
|
@ -44,7 +44,9 @@ export function BeginNode({ selected, data }: NodeProps<NodeData>) {
|
||||
fontSize={24}
|
||||
color={operatorMap[data.label as Operator].color}
|
||||
></OperatorIcon>
|
||||
<div className={styles.nodeTitle}>{t(`flow.begin`)}</div>
|
||||
<div className="truncate text-center font-semibold text-sm">
|
||||
{t(`flow.begin`)}
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex gap={8} vertical className={styles.generateParameters}>
|
||||
{query.map((x, idx) => {
|
||||
|
@ -3,6 +3,7 @@ import { CopyOutlined } from '@ant-design/icons';
|
||||
import { Flex, MenuProps } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Operator } from '../../constant';
|
||||
import { useDuplicateNode } from '../../hooks';
|
||||
import useGraphStore from '../../store';
|
||||
|
||||
@ -15,10 +16,17 @@ interface IProps {
|
||||
const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
||||
const deleteIterationNodeById = useGraphStore(
|
||||
(store) => store.deleteIterationNodeById,
|
||||
);
|
||||
|
||||
const deleteNode = useCallback(() => {
|
||||
deleteNodeById(id);
|
||||
}, [id, deleteNodeById]);
|
||||
if (label === Operator.Iteration) {
|
||||
deleteIterationNodeById(id);
|
||||
} else {
|
||||
deleteNodeById(id);
|
||||
}
|
||||
}, [label, deleteIterationNodeById, id, deleteNodeById]);
|
||||
|
||||
const duplicateNode = useDuplicateNode();
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import { useGetComponentLabelByValue } from '../../hooks';
|
||||
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
import { IGenerateParameter, NodeData } from '../../interface';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
|
@ -1,15 +1,3 @@
|
||||
.commonNode() {
|
||||
box-shadow:
|
||||
-6px 0 12px 0 rgba(179, 177, 177, 0.08),
|
||||
-3px 0 6px -4px rgba(0, 0, 0, 0.12),
|
||||
-6px 0 16px 6px rgba(0, 0, 0, 0.05);
|
||||
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: white;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
background: rgb(63, 63, 63) !important;
|
||||
}
|
||||
@ -43,6 +31,22 @@
|
||||
border: 1.5px solid rgb(59, 118, 244);
|
||||
}
|
||||
|
||||
.selectedIterationNode {
|
||||
border-bottom: 1.5px solid rgb(59, 118, 244);
|
||||
border-left: 1.5px solid rgb(59, 118, 244);
|
||||
border-right: 1.5px solid rgb(59, 118, 244);
|
||||
}
|
||||
|
||||
.iterationHeader {
|
||||
.commonNodeShadow();
|
||||
}
|
||||
|
||||
.selectedHeader {
|
||||
border-top: 1.9px solid rgb(59, 118, 244);
|
||||
border-left: 1.9px solid rgb(59, 118, 244);
|
||||
border-right: 1.9px solid rgb(59, 118, 244);
|
||||
}
|
||||
|
||||
.handle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -133,6 +137,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.iterationNode {
|
||||
.commonNodeShadow();
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.nodeText {
|
||||
padding-inline: 0.4em;
|
||||
padding-block: 0.2em 0.1em;
|
||||
@ -142,12 +152,6 @@
|
||||
.textEllipsis();
|
||||
}
|
||||
|
||||
.nodeTitle {
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
.textEllipsis();
|
||||
}
|
||||
|
||||
.nodeHeader {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
118
web/src/pages/flow/canvas/node/iteration-node.tsx
Normal file
118
web/src/pages/flow/canvas/node/iteration-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -8,15 +8,17 @@ import NodeDropdown from './dropdown';
|
||||
import { NextNodePopover } from './popover';
|
||||
|
||||
import { RunTooltip } from '../../flow-tooltip';
|
||||
import styles from './index.less';
|
||||
interface IProps {
|
||||
id: string;
|
||||
label: string;
|
||||
name: string;
|
||||
gap?: number;
|
||||
className?: string;
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
const ExcludedRunStateOperators = [Operator.Answer];
|
||||
|
||||
export function RunStatus({ id, name, label }: IProps) {
|
||||
const { t } = useTranslate('flow');
|
||||
return (
|
||||
@ -35,10 +37,17 @@ export function RunStatus({ id, name, label }: IProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
|
||||
const NodeHeader = ({
|
||||
label,
|
||||
id,
|
||||
name,
|
||||
gap = 4,
|
||||
className,
|
||||
wrapperClassName,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<section>
|
||||
{label !== Operator.Answer && (
|
||||
<section className={wrapperClassName}>
|
||||
{!ExcludedRunStateOperators.includes(label as Operator) && (
|
||||
<RunStatus id={id} name={name} label={label}></RunStatus>
|
||||
)}
|
||||
<Flex
|
||||
@ -52,7 +61,9 @@ const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
|
||||
name={label as Operator}
|
||||
color={operatorMap[label as Operator].color}
|
||||
></OperatorIcon>
|
||||
<span className={styles.nodeTitle}>{name}</span>
|
||||
<span className="truncate text-center font-semibold text-sm">
|
||||
{name}
|
||||
</span>
|
||||
<NodeDropdown id={id} label={label}></NodeDropdown>
|
||||
</Flex>
|
||||
</section>
|
||||
|
@ -3,7 +3,7 @@ import get from 'lodash/get';
|
||||
import React, { MouseEventHandler, useCallback, useMemo } from 'react';
|
||||
import JsonView from 'react18-json-view';
|
||||
import 'react18-json-view/src/style.css';
|
||||
import { useGetComponentLabelByValue, useReplaceIdWithText } from '../../hooks';
|
||||
import { useReplaceIdWithText } from '../../hooks';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import {
|
||||
@ -20,6 +20,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
|
||||
interface IProps extends React.PropsWithChildren {
|
||||
nodeId: string;
|
||||
|
@ -2,7 +2,7 @@ import { useTheme } from '@/components/theme-provider';
|
||||
import { Divider, Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import { useGetComponentLabelByValue } from '../../hooks';
|
||||
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
import { ISwitchCondition, NodeData } from '../../interface';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
import { useBuildSwitchHandlePositions } from './hooks';
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import { useGetComponentLabelByValue } from '../../hooks';
|
||||
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
import { IGenerateParameter, NodeData } from '../../interface';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import styles from './index.less';
|
||||
|
||||
export function TemplateNode({
|
||||
|
@ -50,7 +50,9 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import {
|
||||
CirclePower,
|
||||
CloudUpload,
|
||||
IterationCcw,
|
||||
ListOrdered,
|
||||
OptionIcon,
|
||||
TextCursorInput,
|
||||
@ -58,6 +60,8 @@ import {
|
||||
WrapText,
|
||||
} from 'lucide-react';
|
||||
|
||||
export const BeginId = 'begin';
|
||||
|
||||
export enum Operator {
|
||||
Begin = 'Begin',
|
||||
Retrieval = 'Retrieval',
|
||||
@ -93,6 +97,8 @@ export enum Operator {
|
||||
Invoke = 'Invoke',
|
||||
Template = 'Template',
|
||||
Email = 'Email',
|
||||
Iteration = 'Iteration',
|
||||
IterationStart = 'IterationItem',
|
||||
}
|
||||
|
||||
export const CommonOperatorList = Object.values(Operator).filter(
|
||||
@ -134,6 +140,8 @@ export const operatorIconMap = {
|
||||
[Operator.Invoke]: InvokeIcon,
|
||||
[Operator.Template]: TemplateIcon,
|
||||
[Operator.Email]: EmailIcon,
|
||||
[Operator.Iteration]: IterationCcw,
|
||||
[Operator.IterationStart]: CirclePower,
|
||||
};
|
||||
|
||||
export const operatorMap: Record<
|
||||
@ -270,6 +278,8 @@ export const operatorMap: Record<
|
||||
backgroundColor: '#dee0e2',
|
||||
},
|
||||
[Operator.Email]: { backgroundColor: '#e6f7ff' },
|
||||
[Operator.Iteration]: { backgroundColor: '#e6f7ff' },
|
||||
[Operator.IterationStart]: { backgroundColor: '#e6f7ff' },
|
||||
};
|
||||
|
||||
export const componentMenuList = [
|
||||
@ -306,6 +316,9 @@ export const componentMenuList = [
|
||||
{
|
||||
name: Operator.Template,
|
||||
},
|
||||
{
|
||||
name: Operator.Iteration,
|
||||
},
|
||||
{
|
||||
name: Operator.Note,
|
||||
},
|
||||
@ -606,6 +619,11 @@ export const initialEmailValues = {
|
||||
content: '',
|
||||
};
|
||||
|
||||
export const initialIterationValues = {
|
||||
delimiter: ',',
|
||||
};
|
||||
export const initialIterationStartValues = {};
|
||||
|
||||
export const CategorizeAnchorPointPositions = [
|
||||
{ top: 1, right: 34 },
|
||||
{ top: 8, right: 18 },
|
||||
@ -687,6 +705,8 @@ export const RestrictedUpstreamMap = {
|
||||
[Operator.Invoke]: [Operator.Begin],
|
||||
[Operator.Template]: [Operator.Begin, Operator.Relevant],
|
||||
[Operator.Email]: [Operator.Begin],
|
||||
[Operator.Iteration]: [Operator.Begin],
|
||||
[Operator.IterationStart]: [Operator.Begin],
|
||||
};
|
||||
|
||||
export const NodeMap = {
|
||||
@ -724,6 +744,8 @@ export const NodeMap = {
|
||||
[Operator.Invoke]: 'invokeNode',
|
||||
[Operator.Template]: 'templateNode',
|
||||
[Operator.Email]: 'emailNode',
|
||||
[Operator.Iteration]: 'group',
|
||||
[Operator.IterationStart]: 'iterationStartNode',
|
||||
};
|
||||
|
||||
export const LanguageOptions = [
|
||||
@ -2940,4 +2962,5 @@ export const NoDebugOperatorsList = [
|
||||
Operator.Message,
|
||||
Operator.RewriteQuestion,
|
||||
Operator.Switch,
|
||||
Operator.Iteration,
|
||||
];
|
||||
|
@ -6,7 +6,7 @@ import { lowerFirst } from 'lodash';
|
||||
import { Play } from 'lucide-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Node } from 'reactflow';
|
||||
import { Operator, operatorMap } from '../constant';
|
||||
import { BeginId, Operator, operatorMap } from '../constant';
|
||||
import AkShareForm from '../form/akshare-form';
|
||||
import AnswerForm from '../form/answer-form';
|
||||
import ArXivForm from '../form/arxiv-form';
|
||||
@ -45,6 +45,7 @@ import { getDrawerWidth, needsSingleStepDebugging } from '../utils';
|
||||
import SingleDebugDrawer from './single-debug-drawer';
|
||||
|
||||
import { RunTooltip } from '../flow-tooltip';
|
||||
import IterationForm from '../form/iteration-from';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
@ -89,6 +90,8 @@ const FormMap = {
|
||||
[Operator.Note]: () => <></>,
|
||||
[Operator.Template]: TemplateForm,
|
||||
[Operator.Email]: EmailForm,
|
||||
[Operator.Iteration]: IterationForm,
|
||||
[Operator.IterationStart]: () => <></>,
|
||||
};
|
||||
|
||||
const EmptyContent = () => <div></div>;
|
||||
@ -137,11 +140,15 @@ const FormDrawer = ({
|
||||
<label htmlFor="" className={styles.title}>
|
||||
{t('title')}
|
||||
</label>
|
||||
<Input
|
||||
value={name}
|
||||
onBlur={handleNameBlur}
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
{node?.id === BeginId ? (
|
||||
<span>{t(BeginId)}</span>
|
||||
) : (
|
||||
<Input
|
||||
value={name}
|
||||
onBlur={handleNameBlur}
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
)}
|
||||
</Flex>
|
||||
{needsSingleStepDebugging(operatorName) && (
|
||||
<RunTooltip>
|
||||
|
@ -12,7 +12,7 @@ const AkShareForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={10} max={99}></TopNItem>
|
||||
</Form>
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ const ArXivForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
|
||||
<TopNItem initialValue={10}></TopNItem>
|
||||
<Form.Item label={t('sortBy')} name={'sort_by'}>
|
||||
|
@ -39,7 +39,7 @@ const BaiduFanyiForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item label={t('appid')} name={'appid'}>
|
||||
<Input></Input>
|
||||
</Form.Item>
|
||||
|
@ -12,7 +12,7 @@ const BaiduForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={10}></TopNItem>
|
||||
</Form>
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ const BingForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={10}></TopNItem>
|
||||
<Form.Item label={t('channel')} name={'channel'}>
|
||||
<Select options={options}></Select>
|
||||
|
@ -24,7 +24,7 @@ const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => {
|
||||
initialValues={{ items: [{}] }}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item
|
||||
name={'llm_id'}
|
||||
label={t('model', { keyPrefix: 'chat' })}
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
|
||||
|
||||
import { PropsWithChildren, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBuildComponentIdSelectOptions } from '../../hooks';
|
||||
import { Node } from 'reactflow';
|
||||
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
|
||||
import { NodeData } from '../../interface';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
nodeId?: string;
|
||||
node?: Node<NodeData>;
|
||||
}
|
||||
|
||||
enum VariableType {
|
||||
@ -18,9 +20,12 @@ enum VariableType {
|
||||
const getVariableName = (type: string) =>
|
||||
type === VariableType.Reference ? 'component_id' : 'value';
|
||||
|
||||
const DynamicVariableForm = ({ nodeId }: IProps) => {
|
||||
const DynamicVariableForm = ({ node }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const valueOptions = useBuildComponentIdSelectOptions(nodeId);
|
||||
const valueOptions = useBuildComponentIdSelectOptions(
|
||||
node?.id,
|
||||
node?.parentId,
|
||||
);
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const options = [
|
||||
@ -114,11 +119,11 @@ export function FormCollapse({
|
||||
);
|
||||
}
|
||||
|
||||
const DynamicInputVariable = ({ nodeId }: IProps) => {
|
||||
const DynamicInputVariable = ({ node }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FormCollapse title={t('flow.input')}>
|
||||
<DynamicVariableForm nodeId={nodeId}></DynamicVariableForm>
|
||||
<DynamicVariableForm node={node}></DynamicVariableForm>
|
||||
</FormCollapse>
|
||||
);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ const CrawlerForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item label={t('proxy')} name={'proxy'}>
|
||||
<Input placeholder="like: http://127.0.0.1:8888"></Input>
|
||||
</Form.Item>
|
||||
|
@ -18,7 +18,7 @@ const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={5}></TopNItem>
|
||||
<Form.Item label={t('authKey')} name={'auth_key'}>
|
||||
<Select options={options}></Select>
|
||||
|
@ -21,7 +21,7 @@ const DuckDuckGoForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={10}></TopNItem>
|
||||
<Form.Item
|
||||
label={t('channel')}
|
||||
|
@ -14,7 +14,7 @@ const EmailForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
|
||||
{/* SMTP服务器配置 */}
|
||||
<Form.Item label={t('smtpServer')} name={'smtp_server'}>
|
||||
|
@ -24,7 +24,7 @@ const ExeSQLForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item
|
||||
label={t('dbType')}
|
||||
name={'db_type'}
|
||||
|
@ -2,14 +2,14 @@ import { EditableCell, EditableRow } from '@/components/editable-cell';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Select, Table, TableProps } from 'antd';
|
||||
import { IGenerateParameter } from '../../interface';
|
||||
|
||||
import { useBuildComponentIdSelectOptions } from '../../hooks';
|
||||
import { Node } from 'reactflow';
|
||||
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
|
||||
import { IGenerateParameter, NodeData } from '../../interface';
|
||||
import { useHandleOperateParameters } from './hooks';
|
||||
import styles from './index.less';
|
||||
|
||||
import styles from './index.less';
|
||||
interface IProps {
|
||||
nodeId?: string;
|
||||
node?: Node<NodeData>;
|
||||
}
|
||||
|
||||
const components = {
|
||||
@ -19,10 +19,11 @@ const components = {
|
||||
},
|
||||
};
|
||||
|
||||
const DynamicParameters = ({ nodeId }: IProps) => {
|
||||
const DynamicParameters = ({ node }: IProps) => {
|
||||
const nodeId = node?.id;
|
||||
const { t } = useTranslate('flow');
|
||||
|
||||
const options = useBuildComponentIdSelectOptions(nodeId);
|
||||
const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId);
|
||||
const {
|
||||
dataSource,
|
||||
handleAdd,
|
||||
|
@ -49,7 +49,7 @@ const GenerateForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
<MessageHistoryWindowSizeItem
|
||||
initialValue={12}
|
||||
></MessageHistoryWindowSizeItem>
|
||||
<DynamicParameters nodeId={node?.id}></DynamicParameters>
|
||||
<DynamicParameters node={node}></DynamicParameters>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ const GithubForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={5}></TopNItem>
|
||||
</Form>
|
||||
);
|
||||
|
@ -16,7 +16,7 @@ const GoogleForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={10}></TopNItem>
|
||||
<Form.Item label={t('apiKey')} name={'api_key'}>
|
||||
<Input></Input>
|
||||
|
@ -45,7 +45,7 @@ const GoogleScholarForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={5}></TopNItem>
|
||||
<Form.Item
|
||||
label={t('sortBy')}
|
||||
|
@ -2,15 +2,16 @@ import { EditableCell, EditableRow } from '@/components/editable-cell';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Button, Collapse, Flex, Input, Select, Table, TableProps } from 'antd';
|
||||
import { useBuildComponentIdSelectOptions } from '../../hooks';
|
||||
import { IInvokeVariable } from '../../interface';
|
||||
import { trim } from 'lodash';
|
||||
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
|
||||
import { IInvokeVariable, NodeData } from '../../interface';
|
||||
import { useHandleOperateParameters } from './hooks';
|
||||
|
||||
import { trim } from 'lodash';
|
||||
import { Node } from 'reactflow';
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
nodeId?: string;
|
||||
node?: Node<NodeData>;
|
||||
}
|
||||
|
||||
const components = {
|
||||
@ -20,10 +21,11 @@ const components = {
|
||||
},
|
||||
};
|
||||
|
||||
const DynamicVariablesForm = ({ nodeId }: IProps) => {
|
||||
const DynamicVariablesForm = ({ node }: IProps) => {
|
||||
const nodeId = node?.id;
|
||||
const { t } = useTranslate('flow');
|
||||
|
||||
const options = useBuildComponentIdSelectOptions(nodeId);
|
||||
const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId);
|
||||
const {
|
||||
dataSource,
|
||||
handleAdd,
|
||||
|
@ -69,7 +69,7 @@ const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<DynamicVariablesForm nodeId={node?.id}></DynamicVariablesForm>
|
||||
<DynamicVariablesForm node={node}></DynamicVariablesForm>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
|
94
web/src/pages/flow/form/iteration-from/index.tsx
Normal file
94
web/src/pages/flow/form/iteration-from/index.tsx
Normal 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;
|
@ -65,7 +65,7 @@ const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item label={t('type')} name={'type'} initialValue={'flash'}>
|
||||
<Select options={jin10TypeOptions}></Select>
|
||||
</Form.Item>
|
||||
|
@ -16,7 +16,7 @@ const KeywordExtractForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item
|
||||
name={'llm_id'}
|
||||
label={t('model', { keyPrefix: 'chat' })}
|
||||
|
@ -15,7 +15,7 @@ const PubMedForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={10}></TopNItem>
|
||||
<Form.Item
|
||||
label={t('email')}
|
||||
|
@ -55,7 +55,7 @@ const QWeatherForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item label={t('webApiKey')} name={'web_apikey'}>
|
||||
<Input></Input>
|
||||
</Form.Item>
|
||||
|
@ -32,7 +32,7 @@ const RetrievalForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
form={form}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<SimilaritySlider
|
||||
isTooltipShown
|
||||
vectorSimilarityWeightName="keywords_similarity_weight"
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
SwitchOperatorOptions,
|
||||
} from '../../constant';
|
||||
import { useBuildFormSelectOptions } from '../../form-hooks';
|
||||
import { useBuildComponentIdSelectOptions } from '../../hooks';
|
||||
import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query';
|
||||
import { IOperatorForm, ISwitchForm } from '../../interface';
|
||||
import { getOtherFieldValues } from '../../utils';
|
||||
|
||||
@ -43,7 +43,10 @@ const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => {
|
||||
}));
|
||||
}, [t]);
|
||||
|
||||
const componentIdOptions = useBuildComponentIdSelectOptions(node?.id);
|
||||
const componentIdOptions = useBuildComponentIdSelectOptions(
|
||||
node?.id,
|
||||
node?.parentId,
|
||||
);
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
@ -18,7 +18,7 @@ const TemplateForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
<Input.TextArea rows={8} placeholder={t('flow.blank')} />
|
||||
</Form.Item>
|
||||
|
||||
<DynamicParameters nodeId={node?.id}></DynamicParameters>
|
||||
<DynamicParameters node={node}></DynamicParameters>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
@ -56,7 +56,7 @@ const TuShareForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item
|
||||
label={t('token')}
|
||||
name={'token'}
|
||||
|
@ -24,7 +24,7 @@ const WenCaiForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={20} max={99}></TopNItem>
|
||||
<Form.Item label={t('queryType')} name={'query_type'}>
|
||||
<Select options={wenCaiQueryTypeOptions}></Select>
|
||||
|
@ -16,7 +16,7 @@ const WikipediaForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<TopNItem initialValue={10}></TopNItem>
|
||||
<Form.Item label={t('language')} name={'language'}>
|
||||
<Select options={LanguageOptions}></Select>
|
||||
|
@ -14,7 +14,7 @@ const YahooFinanceForm = ({ onValuesChange, form, node }: IOperatorForm) => {
|
||||
onValuesChange={onValuesChange}
|
||||
layout={'vertical'}
|
||||
>
|
||||
<DynamicInputVariable nodeId={node?.id}></DynamicInputVariable>
|
||||
<DynamicInputVariable node={node}></DynamicInputVariable>
|
||||
<Form.Item label={t('info')} name={'info'}>
|
||||
<Switch></Switch>
|
||||
</Form.Item>
|
||||
|
@ -10,11 +10,14 @@ import { Link, useParams } from 'umi';
|
||||
import {
|
||||
useGetBeginNodeDataQuery,
|
||||
useGetBeginNodeDataQueryIsEmpty,
|
||||
} from '../hooks/use-get-begin-query';
|
||||
import {
|
||||
useSaveGraph,
|
||||
useSaveGraphBeforeOpeningDebugDrawer,
|
||||
useWatchAgentChange,
|
||||
} from '../hooks';
|
||||
} from '../hooks/use-save-graph';
|
||||
import { BeginQuery } from '../interface';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
interface IProps {
|
||||
|
@ -1,7 +1,3 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useFetchFlow, useResetFlow, useSetFlow } from '@/hooks/flow-hooks';
|
||||
import { IGraph } from '@/interfaces/database/flow';
|
||||
import { useIsFetching } from '@tanstack/react-query';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
useCallback,
|
||||
@ -12,23 +8,17 @@ import React, {
|
||||
import { Connection, Edge, Node, Position, ReactFlowInstance } from 'reactflow';
|
||||
// import { shallow } from 'zustand/shallow';
|
||||
import { variableEnabledFieldMap } from '@/constants/chat';
|
||||
import { FileMimeType } from '@/constants/common';
|
||||
import {
|
||||
ModelVariableType,
|
||||
settledModelVariableMap,
|
||||
} from '@/constants/knowledge';
|
||||
import { useFetchModelId } from '@/hooks/logic-hooks';
|
||||
import { Variable } from '@/interfaces/database/chat';
|
||||
import { downloadJsonFile } from '@/utils/file-util';
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import { FormInstance, UploadFile, message } from 'antd';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import dayjs from 'dayjs';
|
||||
import { FormInstance, message } from 'antd';
|
||||
import { humanId } from 'human-id';
|
||||
import { get, isEmpty, lowerFirst, pick } from 'lodash';
|
||||
import trim from 'lodash/trim';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
NodeMap,
|
||||
@ -53,6 +43,7 @@ import {
|
||||
initialGoogleScholarValues,
|
||||
initialGoogleValues,
|
||||
initialInvokeValues,
|
||||
initialIterationValues,
|
||||
initialJin10Values,
|
||||
initialKeywordExtractValues,
|
||||
initialMessageValues,
|
||||
@ -69,18 +60,13 @@ import {
|
||||
initialWikipediaValues,
|
||||
initialYahooFinanceValues,
|
||||
} from './constant';
|
||||
import {
|
||||
BeginQuery,
|
||||
ICategorizeForm,
|
||||
IRelevantForm,
|
||||
ISwitchForm,
|
||||
} from './interface';
|
||||
import { ICategorizeForm, IRelevantForm, ISwitchForm } from './interface';
|
||||
import useGraphStore, { RFState } from './store';
|
||||
import {
|
||||
buildDslComponentsByGraph,
|
||||
generateNodeNamesWithIncreasingIndex,
|
||||
generateSwitchHandleText,
|
||||
getNodeDragHandle,
|
||||
getRelativePositionToIterationNode,
|
||||
replaceIdWithText,
|
||||
} from './utils';
|
||||
|
||||
@ -145,6 +131,8 @@ export const useInitializeOperatorParams = () => {
|
||||
[Operator.Invoke]: initialInvokeValues,
|
||||
[Operator.Template]: initialTemplateValues,
|
||||
[Operator.Email]: initialEmailValues,
|
||||
[Operator.Iteration]: initialIterationValues,
|
||||
[Operator.IterationStart]: initialIterationValues,
|
||||
};
|
||||
}, [llmId]);
|
||||
|
||||
@ -210,7 +198,7 @@ export const useHandleDrop = () => {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
const newNode = {
|
||||
const newNode: Node<any> = {
|
||||
id: `${type}:${humanId()}`,
|
||||
type: NodeMap[type as Operator] || 'ragNode',
|
||||
position: position || {
|
||||
@ -227,7 +215,38 @@ export const useHandleDrop = () => {
|
||||
dragHandle: getNodeDragHandle(type),
|
||||
};
|
||||
|
||||
addNode(newNode);
|
||||
if (type === Operator.Iteration) {
|
||||
newNode.style = {
|
||||
width: 500,
|
||||
height: 250,
|
||||
};
|
||||
const iterationStartNode: Node<any> = {
|
||||
id: `${Operator.IterationStart}:${humanId()}`,
|
||||
type: 'iterationStartNode',
|
||||
position: { x: 50, y: 100 },
|
||||
// draggable: false,
|
||||
data: {
|
||||
label: Operator.IterationStart,
|
||||
name: Operator.IterationStart,
|
||||
form: {},
|
||||
},
|
||||
parentId: newNode.id,
|
||||
extent: 'parent',
|
||||
};
|
||||
addNode(newNode);
|
||||
addNode(iterationStartNode);
|
||||
} else {
|
||||
const subNodeOfIteration = getRelativePositionToIterationNode(
|
||||
nodes,
|
||||
position,
|
||||
);
|
||||
if (subNodeOfIteration) {
|
||||
newNode.parentId = subNodeOfIteration.parentId;
|
||||
newNode.position = subNodeOfIteration.position;
|
||||
newNode.extent = 'parent';
|
||||
}
|
||||
addNode(newNode);
|
||||
}
|
||||
},
|
||||
[reactFlowInstance, getNodeName, nodes, initializeOperatorParams, addNode],
|
||||
);
|
||||
@ -235,78 +254,6 @@ export const useHandleDrop = () => {
|
||||
return { onDrop, onDragOver, setReactFlowInstance };
|
||||
};
|
||||
|
||||
export const useShowFormDrawer = () => {
|
||||
const {
|
||||
clickedNodeId: clickNodeId,
|
||||
setClickedNodeId,
|
||||
getNode,
|
||||
} = useGraphStore((state) => state);
|
||||
const {
|
||||
visible: formDrawerVisible,
|
||||
hideModal: hideFormDrawer,
|
||||
showModal: showFormDrawer,
|
||||
} = useSetModalState();
|
||||
|
||||
const handleShow = useCallback(
|
||||
(node: Node) => {
|
||||
setClickedNodeId(node.id);
|
||||
showFormDrawer();
|
||||
},
|
||||
[showFormDrawer, setClickedNodeId],
|
||||
);
|
||||
|
||||
return {
|
||||
formDrawerVisible,
|
||||
hideFormDrawer,
|
||||
showFormDrawer: handleShow,
|
||||
clickedNode: getNode(clickNodeId),
|
||||
};
|
||||
};
|
||||
|
||||
export const useBuildDslData = () => {
|
||||
const { data } = useFetchFlow();
|
||||
const { nodes, edges } = useGraphStore((state) => state);
|
||||
|
||||
const buildDslData = useCallback(
|
||||
(currentNodes?: Node[]) => {
|
||||
const dslComponents = buildDslComponentsByGraph(
|
||||
currentNodes ?? nodes,
|
||||
edges,
|
||||
data.dsl.components,
|
||||
);
|
||||
|
||||
return {
|
||||
...data.dsl,
|
||||
graph: { nodes: currentNodes ?? nodes, edges },
|
||||
components: dslComponents,
|
||||
};
|
||||
},
|
||||
[data.dsl, edges, nodes],
|
||||
);
|
||||
|
||||
return { buildDslData };
|
||||
};
|
||||
|
||||
export const useSaveGraph = () => {
|
||||
const { data } = useFetchFlow();
|
||||
const { setFlow, loading } = useSetFlow();
|
||||
const { id } = useParams();
|
||||
const { buildDslData } = useBuildDslData();
|
||||
|
||||
const saveGraph = useCallback(
|
||||
async (currentNodes?: Node[]) => {
|
||||
return setFlow({
|
||||
id,
|
||||
title: data.title,
|
||||
dsl: buildDslData(currentNodes),
|
||||
});
|
||||
},
|
||||
[setFlow, id, data.title, buildDslData],
|
||||
);
|
||||
|
||||
return { saveGraph, loading };
|
||||
};
|
||||
|
||||
export const useHandleFormValuesChange = (id?: string) => {
|
||||
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||
const handleValuesChange = useCallback(
|
||||
@ -335,39 +282,6 @@ export const useHandleFormValuesChange = (id?: string) => {
|
||||
return { handleValuesChange };
|
||||
};
|
||||
|
||||
const useSetGraphInfo = () => {
|
||||
const { setEdges, setNodes } = useGraphStore((state) => state);
|
||||
const setGraphInfo = useCallback(
|
||||
({ nodes = [], edges = [] }: IGraph) => {
|
||||
if (nodes.length || edges.length) {
|
||||
setNodes(nodes);
|
||||
setEdges(edges);
|
||||
}
|
||||
},
|
||||
[setEdges, setNodes],
|
||||
);
|
||||
return setGraphInfo;
|
||||
};
|
||||
|
||||
export const useFetchDataOnMount = () => {
|
||||
const { loading, data, refetch } = useFetchFlow();
|
||||
const setGraphInfo = useSetGraphInfo();
|
||||
|
||||
useEffect(() => {
|
||||
setGraphInfo(data?.dsl?.graph ?? ({} as IGraph));
|
||||
}, [setGraphInfo, data]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
return { loading, flowDetail: data };
|
||||
};
|
||||
|
||||
export const useFlowIsFetching = () => {
|
||||
return useIsFetching({ queryKey: ['flowDetail'] }) > 0;
|
||||
};
|
||||
|
||||
export const useSetLlmSetting = (
|
||||
form?: FormInstance,
|
||||
formData?: Record<string, any>,
|
||||
@ -401,7 +315,22 @@ export const useSetLlmSetting = (
|
||||
};
|
||||
|
||||
export const useValidateConnection = () => {
|
||||
const { edges, getOperatorTypeFromId } = useGraphStore((state) => state);
|
||||
const { edges, getOperatorTypeFromId, getParentIdById } = useGraphStore(
|
||||
(state) => state,
|
||||
);
|
||||
|
||||
const isSameNodeChild = useCallback(
|
||||
(connection: Connection) => {
|
||||
const sourceParentId = getParentIdById(connection.source);
|
||||
const targetParentId = getParentIdById(connection.target);
|
||||
if (sourceParentId || targetParentId) {
|
||||
return sourceParentId === targetParentId;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[getParentIdById],
|
||||
);
|
||||
|
||||
// restricted lines cannot be connected successfully.
|
||||
const isValidConnection = useCallback(
|
||||
(connection: Connection) => {
|
||||
@ -418,10 +347,11 @@ export const useValidateConnection = () => {
|
||||
!hasLine &&
|
||||
RestrictedUpstreamMap[
|
||||
getOperatorTypeFromId(connection.source) as Operator
|
||||
]?.every((x) => x !== getOperatorTypeFromId(connection.target));
|
||||
]?.every((x) => x !== getOperatorTypeFromId(connection.target)) &&
|
||||
isSameNodeChild(connection);
|
||||
return ret;
|
||||
},
|
||||
[edges, getOperatorTypeFromId],
|
||||
[edges, getOperatorTypeFromId, isSameNodeChild],
|
||||
);
|
||||
|
||||
return isValidConnection;
|
||||
@ -464,52 +394,6 @@ export const useHandleNodeNameChange = ({
|
||||
return { name, handleNameBlur, handleNameChange };
|
||||
};
|
||||
|
||||
export const useGetBeginNodeDataQuery = () => {
|
||||
const getNode = useGraphStore((state) => state.getNode);
|
||||
|
||||
const getBeginNodeDataQuery = useCallback(() => {
|
||||
return get(getNode('begin'), 'data.form.query', []);
|
||||
}, [getNode]);
|
||||
|
||||
return getBeginNodeDataQuery;
|
||||
};
|
||||
|
||||
export const useGetBeginNodeDataQueryIsEmpty = () => {
|
||||
const [isBeginNodeDataQueryEmpty, setIsBeginNodeDataQueryEmpty] =
|
||||
useState(false);
|
||||
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
|
||||
const nodes = useGraphStore((state) => state.nodes);
|
||||
|
||||
useEffect(() => {
|
||||
const query: BeginQuery[] = getBeginNodeDataQuery();
|
||||
setIsBeginNodeDataQueryEmpty(query.length === 0);
|
||||
}, [getBeginNodeDataQuery, nodes]);
|
||||
|
||||
return isBeginNodeDataQueryEmpty;
|
||||
};
|
||||
|
||||
export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => {
|
||||
const { saveGraph, loading } = useSaveGraph();
|
||||
const { resetFlow } = useResetFlow();
|
||||
|
||||
const handleRun = useCallback(
|
||||
async (nextNodes?: Node[]) => {
|
||||
const saveRet = await saveGraph(nextNodes);
|
||||
if (saveRet?.code === 0) {
|
||||
// Call the reset api before opening the run drawer each time
|
||||
const resetRet = await resetFlow();
|
||||
// After resetting, all previous messages will be cleared.
|
||||
if (resetRet?.code === 0) {
|
||||
show();
|
||||
}
|
||||
}
|
||||
},
|
||||
[saveGraph, resetFlow, show],
|
||||
);
|
||||
|
||||
return { handleRun, loading };
|
||||
};
|
||||
|
||||
export const useReplaceIdWithName = () => {
|
||||
const getNode = useGraphStore((state) => state.getNode);
|
||||
|
||||
@ -647,66 +531,6 @@ export const useWatchNodeFormDataChange = () => {
|
||||
]);
|
||||
};
|
||||
|
||||
// exclude nodes with branches
|
||||
const ExcludedNodes = [
|
||||
Operator.Categorize,
|
||||
Operator.Relevant,
|
||||
Operator.Begin,
|
||||
Operator.Note,
|
||||
];
|
||||
|
||||
export const useBuildComponentIdSelectOptions = (nodeId?: string) => {
|
||||
const nodes = useGraphStore((state) => state.nodes);
|
||||
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
|
||||
const query: BeginQuery[] = getBeginNodeDataQuery();
|
||||
|
||||
const componentIdOptions = useMemo(() => {
|
||||
return nodes
|
||||
.filter(
|
||||
(x) =>
|
||||
x.id !== nodeId && !ExcludedNodes.some((y) => y === x.data.label),
|
||||
)
|
||||
.map((x) => ({ label: x.data.name, value: x.id }));
|
||||
}, [nodes, nodeId]);
|
||||
|
||||
const groupedOptions = [
|
||||
{
|
||||
label: <span>Component Output</span>,
|
||||
title: 'Component Output',
|
||||
options: componentIdOptions,
|
||||
},
|
||||
{
|
||||
label: <span>Begin Input</span>,
|
||||
title: 'Begin Input',
|
||||
options: query.map((x) => ({
|
||||
label: x.name,
|
||||
value: `begin@${x.key}`,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return groupedOptions;
|
||||
};
|
||||
|
||||
export const useGetComponentLabelByValue = (nodeId: string) => {
|
||||
const options = useBuildComponentIdSelectOptions(nodeId);
|
||||
const flattenOptions = useMemo(
|
||||
() =>
|
||||
options.reduce<DefaultOptionType[]>((pre, cur) => {
|
||||
return [...pre, ...cur.options];
|
||||
}, []),
|
||||
[options],
|
||||
);
|
||||
|
||||
const getLabel = useCallback(
|
||||
(val?: string) => {
|
||||
return flattenOptions.find((x) => x.value === val)?.label;
|
||||
},
|
||||
[flattenOptions],
|
||||
);
|
||||
return getLabel;
|
||||
};
|
||||
|
||||
export const useDuplicateNode = () => {
|
||||
const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
|
||||
const getNodeName = useGetNodeName();
|
||||
@ -769,107 +593,3 @@ export const useCopyPaste = () => {
|
||||
};
|
||||
}, [onPasteCapture]);
|
||||
};
|
||||
|
||||
export const useWatchAgentChange = (chatDrawerVisible: boolean) => {
|
||||
const [time, setTime] = useState<string>();
|
||||
const nodes = useGraphStore((state) => state.nodes);
|
||||
const edges = useGraphStore((state) => state.edges);
|
||||
const { saveGraph } = useSaveGraph();
|
||||
const { data: flowDetail } = useFetchFlow();
|
||||
|
||||
const setSaveTime = useCallback((updateTime: number) => {
|
||||
setTime(dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSaveTime(flowDetail?.update_time);
|
||||
}, [flowDetail, setSaveTime]);
|
||||
|
||||
const saveAgent = useCallback(async () => {
|
||||
if (!chatDrawerVisible) {
|
||||
const ret = await saveGraph();
|
||||
setSaveTime(ret.data.update_time);
|
||||
}
|
||||
}, [chatDrawerVisible, saveGraph, setSaveTime]);
|
||||
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
saveAgent();
|
||||
},
|
||||
[nodes, edges],
|
||||
{
|
||||
wait: 1000 * 20,
|
||||
},
|
||||
);
|
||||
|
||||
return time;
|
||||
};
|
||||
|
||||
export const useHandleExportOrImportJsonFile = () => {
|
||||
const { buildDslData } = useBuildDslData();
|
||||
const {
|
||||
visible: fileUploadVisible,
|
||||
hideModal: hideFileUploadModal,
|
||||
showModal: showFileUploadModal,
|
||||
} = useSetModalState();
|
||||
const setGraphInfo = useSetGraphInfo();
|
||||
const { data } = useFetchFlow();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onFileUploadOk = useCallback(
|
||||
async (fileList: UploadFile[]) => {
|
||||
if (fileList.length > 0) {
|
||||
const file: File = fileList[0] as unknown as File;
|
||||
if (file.type !== FileMimeType.Json) {
|
||||
message.error(t('flow.jsonUploadTypeErrorMessage'));
|
||||
return;
|
||||
}
|
||||
|
||||
const graphStr = await file.text();
|
||||
const errorMessage = t('flow.jsonUploadContentErrorMessage');
|
||||
try {
|
||||
const graph = JSON.parse(graphStr);
|
||||
if (graphStr && !isEmpty(graph) && Array.isArray(graph?.nodes)) {
|
||||
setGraphInfo(graph ?? ({} as IGraph));
|
||||
hideFileUploadModal();
|
||||
} else {
|
||||
message.error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(errorMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
[hideFileUploadModal, setGraphInfo, t],
|
||||
);
|
||||
|
||||
const handleExportJson = useCallback(() => {
|
||||
downloadJsonFile(buildDslData().graph, `${data.title}.json`);
|
||||
}, [buildDslData, data.title]);
|
||||
|
||||
return {
|
||||
fileUploadVisible,
|
||||
handleExportJson,
|
||||
handleImportJson: showFileUploadModal,
|
||||
hideFileUploadModal,
|
||||
onFileUploadOk,
|
||||
};
|
||||
};
|
||||
|
||||
export const useShowSingleDebugDrawer = () => {
|
||||
const { visible, showModal, hideModal } = useSetModalState();
|
||||
const { saveGraph } = useSaveGraph();
|
||||
|
||||
const showSingleDebugDrawer = useCallback(async () => {
|
||||
const saveRet = await saveGraph();
|
||||
if (saveRet?.code === 0) {
|
||||
showModal();
|
||||
}
|
||||
}, [saveGraph, showModal]);
|
||||
|
||||
return {
|
||||
singleDebugDrawerVisible: visible,
|
||||
hideSingleDebugDrawer: hideModal,
|
||||
showSingleDebugDrawer,
|
||||
};
|
||||
};
|
||||
|
29
web/src/pages/flow/hooks/use-build-dsl.ts
Normal file
29
web/src/pages/flow/hooks/use-build-dsl.ts
Normal 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 };
|
||||
};
|
62
web/src/pages/flow/hooks/use-export-json.ts
Normal file
62
web/src/pages/flow/hooks/use-export-json.ts
Normal 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,
|
||||
};
|
||||
};
|
19
web/src/pages/flow/hooks/use-fetch-data.ts
Normal file
19
web/src/pages/flow/hooks/use-fetch-data.ts
Normal 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 };
|
||||
};
|
112
web/src/pages/flow/hooks/use-get-begin-query.tsx
Normal file
112
web/src/pages/flow/hooks/use-get-begin-query.tsx
Normal 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;
|
||||
};
|
0
web/src/pages/flow/hooks/use-iteration.ts
Normal file
0
web/src/pages/flow/hooks/use-iteration.ts
Normal file
85
web/src/pages/flow/hooks/use-save-graph.ts
Normal file
85
web/src/pages/flow/hooks/use-save-graph.ts
Normal 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;
|
||||
};
|
17
web/src/pages/flow/hooks/use-set-graph.ts
Normal file
17
web/src/pages/flow/hooks/use-set-graph.ts
Normal 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;
|
||||
};
|
153
web/src/pages/flow/hooks/use-show-drawer.tsx
Normal file
153
web/src/pages/flow/hooks/use-show-drawer.tsx
Normal 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,
|
||||
};
|
||||
}
|
@ -5,7 +5,8 @@ import { ReactFlowProvider } from 'reactflow';
|
||||
import FlowCanvas from './canvas';
|
||||
import Sider from './flow-sider';
|
||||
import FlowHeader from './header';
|
||||
import { useCopyPaste, useFetchDataOnMount } from './hooks';
|
||||
import { useCopyPaste } from './hooks';
|
||||
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
|
@ -90,7 +90,7 @@ export interface ISwitchForm {
|
||||
export type NodeData = {
|
||||
label: string; // operator type
|
||||
name: string; // operator name
|
||||
color: string;
|
||||
color?: string;
|
||||
form:
|
||||
| IBeginForm
|
||||
| IRetrievalForm
|
||||
|
@ -4,18 +4,8 @@ import {
|
||||
useFetchFlowTemplates,
|
||||
useSetFlow,
|
||||
} from '@/hooks/flow-hooks';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'umi';
|
||||
// import { dsl } from '../mock';
|
||||
// import headhunterZhComponents from '../../../../../graph/test/dsl_examples/headhunter_zh.json';
|
||||
// import dslJson from '../../../../../dls.json';
|
||||
// import customerServiceBase from '../../../../../graph/test/dsl_examples/customer_service.json';
|
||||
// import customerService from '../customer_service.json';
|
||||
// import interpreterBase from '../../../../../graph/test/dsl_examples/interpreter.json';
|
||||
// import interpreter from '../interpreter.json';
|
||||
|
||||
// import retrievalRelevantRewriteAndGenerateBase from '../../../../../graph/test/dsl_examples/retrieval_relevant_rewrite_and_generate.json';
|
||||
// import retrievalRelevantRewriteAndGenerate from '../retrieval_relevant_rewrite_and_generate.json';
|
||||
|
||||
export const useFetchDataOnMount = () => {
|
||||
const { data, loading } = useFetchFlowList();
|
||||
@ -24,7 +14,6 @@ export const useFetchDataOnMount = () => {
|
||||
};
|
||||
|
||||
export const useSaveFlow = () => {
|
||||
const [currentFlow, setCurrentFlow] = useState({});
|
||||
const {
|
||||
visible: flowSettingVisible,
|
||||
hideModal: hideFlowSettingModal,
|
||||
@ -39,18 +28,10 @@ export const useSaveFlow = () => {
|
||||
const templateItem = list.find((x) => x.id === templateId);
|
||||
|
||||
let dsl = templateItem?.dsl;
|
||||
// if (dsl) {
|
||||
// dsl.graph = headhunter_zh;
|
||||
// }
|
||||
const ret = await setFlow({
|
||||
title,
|
||||
dsl,
|
||||
avatar: templateItem?.avatar,
|
||||
// dsl: dslJson,
|
||||
// dsl: {
|
||||
// ...retrievalRelevantRewriteAndGenerateBase,
|
||||
// graph: retrievalRelevantRewriteAndGenerate,
|
||||
// },
|
||||
});
|
||||
|
||||
if (ret?.code === 0) {
|
||||
@ -61,20 +42,12 @@ export const useSaveFlow = () => {
|
||||
[setFlow, hideFlowSettingModal, navigate, list],
|
||||
);
|
||||
|
||||
const handleShowFlowSettingModal = useCallback(
|
||||
async (record: any) => {
|
||||
setCurrentFlow(record);
|
||||
showFileRenameModal();
|
||||
},
|
||||
[showFileRenameModal],
|
||||
);
|
||||
|
||||
return {
|
||||
flowSettingLoading: loading,
|
||||
initialFlowName: '',
|
||||
onFlowOk,
|
||||
flowSettingVisible,
|
||||
hideFlowSettingModal,
|
||||
showFlowSettingModal: handleShowFlowSettingModal,
|
||||
showFlowSettingModal: showFileRenameModal,
|
||||
};
|
||||
};
|
||||
|
@ -2,16 +2,14 @@ import { IModalProps } from '@/interfaces/common';
|
||||
import { Drawer } from 'antd';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useGetBeginNodeDataQuery,
|
||||
useSaveGraphBeforeOpeningDebugDrawer,
|
||||
} from '../hooks';
|
||||
import { BeginId } from '../constant';
|
||||
import DebugContent from '../debug-content';
|
||||
import { useGetBeginNodeDataQuery } from '../hooks/use-get-begin-query';
|
||||
import { useSaveGraphBeforeOpeningDebugDrawer } from '../hooks/use-save-graph';
|
||||
import { BeginQuery } from '../interface';
|
||||
import useGraphStore from '../store';
|
||||
import { getDrawerWidth } from '../utils';
|
||||
|
||||
import DebugContent from '../debug-content';
|
||||
|
||||
const RunDrawer = ({
|
||||
hideModal,
|
||||
showModal: showChatModal,
|
||||
@ -28,7 +26,7 @@ const RunDrawer = ({
|
||||
|
||||
const handleRunAgent = useCallback(
|
||||
(nextValues: Record<string, any>) => {
|
||||
const currentNodes = updateNodeForm('begin', nextValues, ['query']);
|
||||
const currentNodes = updateNodeForm(BeginId, nextValues, ['query']);
|
||||
handleRun(currentNodes);
|
||||
hideModal?.();
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type {} from '@redux-devtools/extension';
|
||||
import { humanId } from 'human-id';
|
||||
import { omit } from 'lodash';
|
||||
import differenceWith from 'lodash/differenceWith';
|
||||
import intersectionWith from 'lodash/intersectionWith';
|
||||
import lodashSet from 'lodash/set';
|
||||
@ -25,8 +25,8 @@ import { Operator, SwitchElseTo } from './constant';
|
||||
import { NodeData } from './interface';
|
||||
import {
|
||||
duplicateNodeForm,
|
||||
generateDuplicateNode,
|
||||
generateNodeNamesWithIncreasingIndex,
|
||||
getNodeDragHandle,
|
||||
getOperatorIndex,
|
||||
isEdgeEqual,
|
||||
} from './utils';
|
||||
@ -61,13 +61,16 @@ export type RFState = {
|
||||
) => void;
|
||||
deletePreviousEdgeOfClassificationNode: (connection: Connection) => void;
|
||||
duplicateNode: (id: string, name: string) => void;
|
||||
duplicateIterationNode: (id: string, name: string) => void;
|
||||
deleteEdge: () => void;
|
||||
deleteEdgeById: (id: string) => void;
|
||||
deleteNodeById: (id: string) => void;
|
||||
deleteIterationNodeById: (id: string) => void;
|
||||
deleteEdgeBySourceAndSourceHandle: (connection: Partial<Connection>) => void;
|
||||
findNodeByName: (operatorName: Operator) => Node | undefined;
|
||||
updateMutableNodeFormItem: (id: string, field: string, value: any) => void;
|
||||
getOperatorTypeFromId: (id?: string | null) => string | undefined;
|
||||
getParentIdById: (id?: string | null) => string | undefined;
|
||||
updateNodeName: (id: string, name: string) => void;
|
||||
generateNodeName: (name: string) => string;
|
||||
setClickedNodeId: (id?: string) => void;
|
||||
@ -170,6 +173,9 @@ const useGraphStore = create<RFState>()(
|
||||
getOperatorTypeFromId: (id?: string | null) => {
|
||||
return get().getNode(id)?.data?.label;
|
||||
},
|
||||
getParentIdById: (id?: string | null) => {
|
||||
return get().getNode(id)?.parentId;
|
||||
},
|
||||
addEdge: (connection: Connection) => {
|
||||
set({
|
||||
edges: addEdge(connection, get().edges),
|
||||
@ -234,12 +240,14 @@ const useGraphStore = create<RFState>()(
|
||||
}
|
||||
},
|
||||
duplicateNode: (id: string, name: string) => {
|
||||
const { getNode, addNode, generateNodeName } = get();
|
||||
const { getNode, addNode, generateNodeName, duplicateIterationNode } =
|
||||
get();
|
||||
const node = getNode(id);
|
||||
const position = {
|
||||
x: (node?.position?.x || 0) + 50,
|
||||
y: (node?.position?.y || 0) + 50,
|
||||
};
|
||||
|
||||
if (node?.data.label === Operator.Iteration) {
|
||||
duplicateIterationNode(id, name);
|
||||
return;
|
||||
}
|
||||
|
||||
addNode({
|
||||
...(node || {}),
|
||||
@ -247,13 +255,38 @@ const useGraphStore = create<RFState>()(
|
||||
...duplicateNodeForm(node?.data),
|
||||
name: generateNodeName(name),
|
||||
},
|
||||
selected: false,
|
||||
dragging: false,
|
||||
id: `${node?.data?.label}:${humanId()}`,
|
||||
position,
|
||||
dragHandle: getNodeDragHandle(node?.data?.label),
|
||||
...generateDuplicateNode(node?.position, node?.data?.label),
|
||||
});
|
||||
},
|
||||
duplicateIterationNode: (id: string, name: string) => {
|
||||
const { getNode, generateNodeName, nodes } = get();
|
||||
const node = getNode(id);
|
||||
|
||||
const iterationNode: Node<NodeData> = {
|
||||
...(node || {}),
|
||||
data: {
|
||||
...(node?.data || { label: Operator.Iteration, form: {} }),
|
||||
name: generateNodeName(name),
|
||||
},
|
||||
...generateDuplicateNode(node?.position, node?.data?.label),
|
||||
};
|
||||
|
||||
const children = nodes
|
||||
.filter((x) => x.parentId === node?.id)
|
||||
.map((x) => ({
|
||||
...(x || {}),
|
||||
data: {
|
||||
...duplicateNodeForm(x?.data),
|
||||
name: generateNodeName(x.data.name),
|
||||
},
|
||||
...omit(generateDuplicateNode(x?.position, x?.data?.label), [
|
||||
'position',
|
||||
]),
|
||||
parentId: iterationNode.id,
|
||||
}));
|
||||
|
||||
set({ nodes: nodes.concat(iterationNode, ...children) });
|
||||
},
|
||||
deleteEdge: () => {
|
||||
const { edges, selectedEdgeIds } = get();
|
||||
set({
|
||||
@ -323,6 +356,21 @@ const useGraphStore = create<RFState>()(
|
||||
.filter((edge) => edge.target !== id),
|
||||
});
|
||||
},
|
||||
deleteIterationNodeById: (id: string) => {
|
||||
const { nodes, edges } = get();
|
||||
const children = nodes.filter((node) => node.parentId === id);
|
||||
set({
|
||||
nodes: nodes.filter((node) => node.id !== id && node.parentId !== id),
|
||||
edges: edges.filter(
|
||||
(edge) =>
|
||||
edge.source !== id &&
|
||||
edge.target !== id &&
|
||||
!children.some(
|
||||
(child) => edge.source === child.id && edge.target === child.id,
|
||||
),
|
||||
),
|
||||
});
|
||||
},
|
||||
findNodeByName: (name: Operator) => {
|
||||
return get().nodes.find((x) => x.data.label === name);
|
||||
},
|
||||
|
@ -5,7 +5,7 @@ import { humanId } from 'human-id';
|
||||
import { curry, get, intersectionWith, isEqual, sample } from 'lodash';
|
||||
import pipe from 'lodash/fp/pipe';
|
||||
import isObject from 'lodash/isObject';
|
||||
import { Edge, Node, Position } from 'reactflow';
|
||||
import { Edge, Node, Position, XYPosition } from 'reactflow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
CategorizeAnchorPointPositions,
|
||||
@ -144,6 +144,7 @@ export const buildDslComponentsByGraph = (
|
||||
},
|
||||
downstream: buildComponentDownstreamOrUpstream(edges, id, true),
|
||||
upstream: buildComponentDownstreamOrUpstream(edges, id, false),
|
||||
parent_id: x?.parentId,
|
||||
};
|
||||
});
|
||||
|
||||
@ -332,3 +333,55 @@ export const getDrawerWidth = () => {
|
||||
export const needsSingleStepDebugging = (label: string) => {
|
||||
return !NoDebugOperatorsList.some((x) => (label as Operator) === x);
|
||||
};
|
||||
|
||||
// Get the coordinates of the node relative to the Iteration node
|
||||
export function getRelativePositionToIterationNode(
|
||||
nodes: Node<NodeData>[],
|
||||
position?: XYPosition, // relative position
|
||||
) {
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iterationNodes = nodes.filter(
|
||||
(node) => node.data.label === Operator.Iteration,
|
||||
);
|
||||
|
||||
for (const iterationNode of iterationNodes) {
|
||||
const {
|
||||
position: { x, y },
|
||||
width,
|
||||
height,
|
||||
} = iterationNode;
|
||||
const halfWidth = (width || 0) / 2;
|
||||
if (
|
||||
position.x >= x - halfWidth &&
|
||||
position.x <= x + halfWidth &&
|
||||
position.y >= y &&
|
||||
position.y <= y + (height || 0)
|
||||
) {
|
||||
return {
|
||||
parentId: iterationNode.id,
|
||||
position: { x: position.x - x + halfWidth, y: position.y - y },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const generateDuplicateNode = (
|
||||
position?: XYPosition,
|
||||
label?: string,
|
||||
) => {
|
||||
const nextPosition = {
|
||||
x: (position?.x || 0) + 50,
|
||||
y: (position?.y || 0) + 50,
|
||||
};
|
||||
|
||||
return {
|
||||
selected: false,
|
||||
dragging: false,
|
||||
id: `${label}:${humanId()}`,
|
||||
position: nextPosition,
|
||||
dragHandle: getNodeDragHandle(label),
|
||||
};
|
||||
};
|
||||
|
@ -38,7 +38,6 @@ const KnowledgeList = () => {
|
||||
handleInputChange,
|
||||
loading,
|
||||
} = useInfiniteFetchKnowledgeList();
|
||||
console.log('🚀 ~ KnowledgeList ~ data:', data);
|
||||
const nextList = data?.pages?.flatMap((x) => x.kbs) ?? [];
|
||||
|
||||
const total = useMemo(() => {
|
||||
|
5
web/src/pages/workflow.less
Normal file
5
web/src/pages/workflow.less
Normal file
@ -0,0 +1,5 @@
|
||||
.react-flow-subflows-example {
|
||||
.react-flow__node-group {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
151
web/src/pages/workflow.tsx
Normal file
151
web/src/pages/workflow.tsx
Normal 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;
|
@ -246,6 +246,11 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/workflow',
|
||||
component: '@/pages/workflow',
|
||||
layout: false,
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
Loading…
x
Reference in New Issue
Block a user