mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +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:
@ -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;
|
||||
Reference in New Issue
Block a user