mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-08-14 04:05:58 +08:00
### What problem does this PR solve? Feat: Render agent details #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
parent
ca865df87f
commit
fda9b58ab7
@ -42,9 +42,12 @@ export const useNavigatePage = () => {
|
||||
navigate(Routes.Agents);
|
||||
}, [navigate]);
|
||||
|
||||
const navigateToAgent = useCallback(() => {
|
||||
navigate(Routes.Agent);
|
||||
}, [navigate]);
|
||||
const navigateToAgent = useCallback(
|
||||
(id: string) => () => {
|
||||
navigate(`${Routes.Agent}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const navigateToAgentTemplates = useCallback(() => {
|
||||
navigate(Routes.AgentTemplates);
|
||||
|
18
web/src/pages/agent/canvas/context-menu/index.less
Normal file
18
web/src/pages/agent/canvas/context-menu/index.less
Normal file
@ -0,0 +1,18 @@
|
||||
.contextMenu {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-style: solid;
|
||||
box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
button {
|
||||
border: none;
|
||||
display: block;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
107
web/src/pages/agent/canvas/context-menu/index.tsx
Normal file
107
web/src/pages/agent/canvas/context-menu/index.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { NodeMouseHandler, useReactFlow } from '@xyflow/react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
export interface INodeContextMenu {
|
||||
id: string;
|
||||
top: number;
|
||||
left: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function NodeContextMenu({
|
||||
id,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
...props
|
||||
}: INodeContextMenu) {
|
||||
const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
|
||||
|
||||
const duplicateNode = useCallback(() => {
|
||||
const node = getNode(id);
|
||||
const position = {
|
||||
x: node?.position?.x || 0 + 50,
|
||||
y: node?.position?.y || 0 + 50,
|
||||
};
|
||||
|
||||
addNodes({
|
||||
...(node || {}),
|
||||
data: node?.data,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
id: `${node?.id}-copy`,
|
||||
position,
|
||||
});
|
||||
}, [id, getNode, addNodes]);
|
||||
|
||||
const deleteNode = useCallback(() => {
|
||||
setNodes((nodes) => nodes.filter((node) => node.id !== id));
|
||||
setEdges((edges) => edges.filter((edge) => edge.source !== id));
|
||||
}, [id, setNodes, setEdges]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ top, left, right, bottom }}
|
||||
className={styles.contextMenu}
|
||||
{...props}
|
||||
>
|
||||
<p style={{ margin: '0.5em' }}>
|
||||
<small>node: {id}</small>
|
||||
</p>
|
||||
<button onClick={duplicateNode} type={'button'}>
|
||||
duplicate
|
||||
</button>
|
||||
<button onClick={deleteNode} type={'button'}>
|
||||
delete
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* @deprecated
|
||||
*/
|
||||
export const useHandleNodeContextMenu = (sideWidth: number) => {
|
||||
const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu);
|
||||
const ref = useRef<any>(null);
|
||||
|
||||
const onNodeContextMenu: NodeMouseHandler = useCallback(
|
||||
(event, node) => {
|
||||
// Prevent native context menu from showing
|
||||
event.preventDefault();
|
||||
|
||||
// Calculate position of the context menu. We want to make sure it
|
||||
// doesn't get positioned off-screen.
|
||||
const pane = ref.current?.getBoundingClientRect();
|
||||
// setMenu({
|
||||
// id: node.id,
|
||||
// top: event.clientY < pane.height - 200 ? event.clientY : 0,
|
||||
// left: event.clientX < pane.width - 200 ? event.clientX : 0,
|
||||
// right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : 0,
|
||||
// bottom:
|
||||
// event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
|
||||
// });
|
||||
|
||||
setMenu({
|
||||
id: node.id,
|
||||
top: event.clientY - 144,
|
||||
left: event.clientX - sideWidth,
|
||||
// top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
|
||||
// left: event.clientX < pane.width - 200 ? event.clientX : 0,
|
||||
});
|
||||
},
|
||||
[sideWidth],
|
||||
);
|
||||
|
||||
// Close the context menu if it's open whenever the window is clicked.
|
||||
const onPaneClick = useCallback(
|
||||
() => setMenu({} as INodeContextMenu),
|
||||
[setMenu],
|
||||
);
|
||||
|
||||
return { onNodeContextMenu, menu, onPaneClick, ref };
|
||||
};
|
31
web/src/pages/agent/canvas/edge/index.less
Normal file
31
web/src/pages/agent/canvas/edge/index.less
Normal file
@ -0,0 +1,31 @@
|
||||
.edgeButton {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #eee;
|
||||
border: 1px solid #fff;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.edgeButton:hover {
|
||||
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.edgeButtonDark {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #0e0c0c;
|
||||
border: 1px solid #fff;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.edgeButtonDark:hover {
|
||||
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
108
web/src/pages/agent/canvas/edge/index.tsx
Normal file
108
web/src/pages/agent/canvas/edge/index.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getBezierPath,
|
||||
} from '@xyflow/react';
|
||||
import useGraphStore from '../../store';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { useFetchFlow } from '@/hooks/flow-hooks';
|
||||
import { useMemo } from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
export function ButtonEdge({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
source,
|
||||
target,
|
||||
style = {},
|
||||
markerEnd,
|
||||
selected,
|
||||
}: EdgeProps) {
|
||||
const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
const { theme } = useTheme();
|
||||
const selectedStyle = useMemo(() => {
|
||||
return selected ? { strokeWidth: 2, stroke: '#1677ff' } : {};
|
||||
}, [selected]);
|
||||
|
||||
const onEdgeClick = () => {
|
||||
deleteEdgeById(id);
|
||||
};
|
||||
|
||||
// highlight the nodes that the workflow passes through
|
||||
const { data: flowDetail } = useFetchFlow();
|
||||
|
||||
const graphPath = useMemo(() => {
|
||||
// TODO: this will be called multiple times
|
||||
const path = flowDetail?.dsl?.path ?? [];
|
||||
// The second to last
|
||||
const previousGraphPath: string[] = path.at(-2) ?? [];
|
||||
let graphPath: string[] = path.at(-1) ?? [];
|
||||
// The last of the second to last article
|
||||
const previousLatestElement = previousGraphPath.at(-1);
|
||||
if (previousGraphPath.length > 0 && previousLatestElement) {
|
||||
graphPath = [previousLatestElement, ...graphPath];
|
||||
}
|
||||
return graphPath;
|
||||
}, [flowDetail.dsl?.path]);
|
||||
|
||||
const highlightStyle = useMemo(() => {
|
||||
const idx = graphPath.findIndex((x) => x === source);
|
||||
if (idx !== -1) {
|
||||
// The set of elements following source
|
||||
const slicedGraphPath = graphPath.slice(idx + 1);
|
||||
if (slicedGraphPath.some((x) => x === target)) {
|
||||
return { strokeWidth: 2, stroke: 'red' };
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}, [source, target, graphPath]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{ ...style, ...selectedStyle, ...highlightStyle }}
|
||||
/>
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
fontSize: 12,
|
||||
// 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"
|
||||
>
|
||||
<button
|
||||
className={
|
||||
theme === 'dark' ? styles.edgeButtonDark : styles.edgeButton
|
||||
}
|
||||
type="button"
|
||||
onClick={onEdgeClick}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
}
|
10
web/src/pages/agent/canvas/index.less
Normal file
10
web/src/pages/agent/canvas/index.less
Normal file
@ -0,0 +1,10 @@
|
||||
.canvasWrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
:global(.react-flow__node-group) {
|
||||
.commonNode();
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
237
web/src/pages/agent/canvas/index.tsx
Normal file
237
web/src/pages/agent/canvas/index.tsx
Normal file
@ -0,0 +1,237 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
ControlButton,
|
||||
Controls,
|
||||
NodeTypes,
|
||||
ReactFlow,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { Book, FolderInput, FolderOutput } from 'lucide-react';
|
||||
// import ChatDrawer from '../chat/drawer';
|
||||
// import FormDrawer from '../flow-drawer';
|
||||
import {
|
||||
useHandleDrop,
|
||||
useSelectCanvasData,
|
||||
useValidateConnection,
|
||||
useWatchNodeFormDataChange,
|
||||
} from '../hooks';
|
||||
import { useBeforeDelete } from '../hooks/use-before-delete';
|
||||
import { useHandleExportOrImportJsonFile } from '../hooks/use-export-json';
|
||||
import { useOpenDocument } from '../hooks/use-open-document';
|
||||
import { useShowDrawer } from '../hooks/use-show-drawer';
|
||||
// import JsonUploadModal from '../json-upload-modal';
|
||||
// import RunDrawer from '../run-drawer';
|
||||
import { ButtonEdge } from './edge';
|
||||
import styles from './index.less';
|
||||
import { RagNode } from './node';
|
||||
import { BeginNode } from './node/begin-node';
|
||||
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';
|
||||
import NoteNode from './node/note-node';
|
||||
import { RelevantNode } from './node/relevant-node';
|
||||
import { RetrievalNode } from './node/retrieval-node';
|
||||
import { RewriteNode } from './node/rewrite-node';
|
||||
import { SwitchNode } from './node/switch-node';
|
||||
import { TemplateNode } from './node/template-node';
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
ragNode: RagNode,
|
||||
categorizeNode: CategorizeNode,
|
||||
beginNode: BeginNode,
|
||||
relevantNode: RelevantNode,
|
||||
logicNode: LogicNode,
|
||||
noteNode: NoteNode,
|
||||
switchNode: SwitchNode,
|
||||
generateNode: GenerateNode,
|
||||
retrievalNode: RetrievalNode,
|
||||
messageNode: MessageNode,
|
||||
rewriteNode: RewriteNode,
|
||||
keywordNode: KeywordNode,
|
||||
invokeNode: InvokeNode,
|
||||
templateNode: TemplateNode,
|
||||
emailNode: EmailNode,
|
||||
group: IterationNode,
|
||||
iterationStartNode: IterationStartNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
buttonEdge: ButtonEdge,
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
drawerVisible: boolean;
|
||||
hideDrawer(): void;
|
||||
}
|
||||
|
||||
function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
onConnect,
|
||||
onEdgesChange,
|
||||
onNodesChange,
|
||||
onSelectionChange,
|
||||
} = useSelectCanvasData();
|
||||
const isValidConnection = useValidateConnection();
|
||||
|
||||
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
|
||||
|
||||
const {
|
||||
handleExportJson,
|
||||
handleImportJson,
|
||||
fileUploadVisible,
|
||||
onFileUploadOk,
|
||||
hideFileUploadModal,
|
||||
} = useHandleExportOrImportJsonFile();
|
||||
|
||||
const openDocument = useOpenDocument();
|
||||
|
||||
const {
|
||||
onNodeClick,
|
||||
onPaneClick,
|
||||
clickedNode,
|
||||
formDrawerVisible,
|
||||
hideFormDrawer,
|
||||
singleDebugDrawerVisible,
|
||||
hideSingleDebugDrawer,
|
||||
showSingleDebugDrawer,
|
||||
chatVisible,
|
||||
runVisible,
|
||||
hideRunOrChatDrawer,
|
||||
showChatModal,
|
||||
} = useShowDrawer({
|
||||
drawerVisible,
|
||||
hideDrawer,
|
||||
});
|
||||
|
||||
const { handleBeforeDelete } = useBeforeDelete();
|
||||
|
||||
useWatchNodeFormDataChange();
|
||||
|
||||
return (
|
||||
<div className={styles.canvasWrapper}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ position: 'absolute', top: 10, left: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<marker
|
||||
fill="rgb(157 149 225)"
|
||||
id="logo"
|
||||
viewBox="0 0 40 40"
|
||||
refX="8"
|
||||
refY="5"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="20"
|
||||
markerHeight="20"
|
||||
orient="auto-start-reverse"
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
<ReactFlow
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
nodes={nodes}
|
||||
onNodesChange={onNodesChange}
|
||||
edges={edges}
|
||||
onEdgesChange={onEdgesChange}
|
||||
fitView
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onInit={setReactFlowInstance}
|
||||
onSelectionChange={onSelectionChange}
|
||||
nodeOrigin={[0.5, 0]}
|
||||
isValidConnection={isValidConnection}
|
||||
defaultEdgeOptions={{
|
||||
type: 'buttonEdge',
|
||||
markerEnd: 'logo',
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
stroke: 'rgb(202 197 245)',
|
||||
},
|
||||
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
|
||||
}}
|
||||
deleteKeyCode={['Delete', 'Backspace']}
|
||||
onBeforeDelete={handleBeforeDelete}
|
||||
>
|
||||
<Background />
|
||||
<Controls className="text-black !flex-col-reverse">
|
||||
<ControlButton onClick={handleImportJson}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FolderInput className="!fill-none" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Import</TooltipContent>
|
||||
</Tooltip>
|
||||
</ControlButton>
|
||||
<ControlButton onClick={handleExportJson}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FolderOutput className="!fill-none" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Export</TooltipContent>
|
||||
</Tooltip>
|
||||
</ControlButton>
|
||||
<ControlButton onClick={openDocument}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Book className="!fill-none" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Document</TooltipContent>
|
||||
</Tooltip>
|
||||
</ControlButton>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
{/* {formDrawerVisible && (
|
||||
<FormDrawer
|
||||
node={clickedNode}
|
||||
visible={formDrawerVisible}
|
||||
hideModal={hideFormDrawer}
|
||||
singleDebugDrawerVisible={singleDebugDrawerVisible}
|
||||
hideSingleDebugDrawer={hideSingleDebugDrawer}
|
||||
showSingleDebugDrawer={showSingleDebugDrawer}
|
||||
></FormDrawer>
|
||||
)} */}
|
||||
{/* {chatVisible && (
|
||||
<ChatDrawer
|
||||
visible={chatVisible}
|
||||
hideModal={hideRunOrChatDrawer}
|
||||
></ChatDrawer>
|
||||
)}
|
||||
|
||||
{runVisible && (
|
||||
<RunDrawer
|
||||
hideModal={hideRunOrChatDrawer}
|
||||
showModal={showChatModal}
|
||||
></RunDrawer>
|
||||
)}
|
||||
{fileUploadVisible && (
|
||||
<JsonUploadModal
|
||||
onOk={onFileUploadOk}
|
||||
visible={fileUploadVisible}
|
||||
hideModal={hideFileUploadModal}
|
||||
></JsonUploadModal>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FlowCanvas;
|
72
web/src/pages/agent/canvas/node/begin-node.tsx
Normal file
72
web/src/pages/agent/canvas/node/begin-node.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IBeginNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
BeginQueryType,
|
||||
BeginQueryTypeIconMap,
|
||||
Operator,
|
||||
operatorMap,
|
||||
} from '../../constant';
|
||||
import { BeginQuery } from '../../interface';
|
||||
import OperatorIcon from '../../operator-icon';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
|
||||
// TODO: do not allow other nodes to connect to this node
|
||||
export function BeginNode({ selected, data }: NodeProps<IBeginNode>) {
|
||||
const { t } = useTranslation();
|
||||
const query: BeginQuery[] = get(data, 'form.query', []);
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.ragNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
></Handle>
|
||||
|
||||
<Flex align="center" justify={'center'} gap={10}>
|
||||
<OperatorIcon
|
||||
name={data.label as Operator}
|
||||
fontSize={24}
|
||||
color={operatorMap[data.label as Operator].color}
|
||||
></OperatorIcon>
|
||||
<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) => {
|
||||
const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType];
|
||||
return (
|
||||
<Flex
|
||||
key={idx}
|
||||
align="center"
|
||||
gap={6}
|
||||
className={styles.conditionBlock}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<label htmlFor="">{x.key}</label>
|
||||
<span className={styles.parameterValue}>{x.name}</span>
|
||||
<span className="flex-1">{x.optional ? 'Yes' : 'No'}</span>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
57
web/src/pages/agent/canvas/node/card.tsx
Normal file
57
web/src/pages/agent/canvas/node/card.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
export function CardWithForm() {
|
||||
return (
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Create project</CardTitle>
|
||||
<CardDescription>Deploy your new project in one-click.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form>
|
||||
<div className="grid w-full items-center gap-4">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" placeholder="Name of your project" />
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Label htmlFor="framework">Framework</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="framework">
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="next">Next.js</SelectItem>
|
||||
<SelectItem value="sveltekit">SvelteKit</SelectItem>
|
||||
<SelectItem value="astro">Astro</SelectItem>
|
||||
<SelectItem value="nuxt">Nuxt.js</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Deploy</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
40
web/src/pages/agent/canvas/node/categorize-handle.tsx
Normal file
40
web/src/pages/agent/canvas/node/categorize-handle.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
|
||||
import React from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
const DEFAULT_HANDLE_STYLE = {
|
||||
width: 6,
|
||||
height: 6,
|
||||
bottom: -5,
|
||||
fontSize: 8,
|
||||
};
|
||||
|
||||
interface IProps extends React.PropsWithChildren {
|
||||
top: number;
|
||||
right: number;
|
||||
id: string;
|
||||
idx?: number;
|
||||
}
|
||||
|
||||
const CategorizeHandle = ({ top, right, id, children }: IProps) => {
|
||||
return (
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={id}
|
||||
isConnectable
|
||||
style={{
|
||||
...DEFAULT_HANDLE_STYLE,
|
||||
top: `${top}%`,
|
||||
right: `${right}%`,
|
||||
background: 'red',
|
||||
color: 'black',
|
||||
}}
|
||||
>
|
||||
<span className={styles.categorizeAnchorPointText}>{children || id}</span>
|
||||
</Handle>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategorizeHandle;
|
68
web/src/pages/agent/canvas/node/categorize-node.tsx
Normal file
68
web/src/pages/agent/canvas/node/categorize-node.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { ICategorizeNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
import { useBuildCategorizeHandlePositions } from './hooks';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function CategorizeNode({
|
||||
id,
|
||||
data,
|
||||
selected,
|
||||
}: NodeProps<ICategorizeNode>) {
|
||||
const { positions } = useBuildCategorizeHandlePositions({ data, id });
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
id={'a'}
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<Flex vertical gap={8}>
|
||||
<div className={styles.nodeText}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
{positions.map((position, idx) => {
|
||||
return (
|
||||
<div key={idx}>
|
||||
<div className={styles.nodeText}>{position.text}</div>
|
||||
<Handle
|
||||
key={position.text}
|
||||
id={position.text}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
style={{ ...RightHandleStyle, top: position.top }}
|
||||
></Handle>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
58
web/src/pages/agent/canvas/node/dropdown.tsx
Normal file
58
web/src/pages/agent/canvas/node/dropdown.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import OperateDropdown from '@/components/operate-dropdown';
|
||||
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';
|
||||
|
||||
interface IProps {
|
||||
id: string;
|
||||
iconFontColor?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
||||
const deleteIterationNodeById = useGraphStore(
|
||||
(store) => store.deleteIterationNodeById,
|
||||
);
|
||||
|
||||
const deleteNode = useCallback(() => {
|
||||
if (label === Operator.Iteration) {
|
||||
deleteIterationNodeById(id);
|
||||
} else {
|
||||
deleteNodeById(id);
|
||||
}
|
||||
}, [label, deleteIterationNodeById, id, deleteNodeById]);
|
||||
|
||||
const duplicateNode = useDuplicateNode();
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '2',
|
||||
onClick: () => duplicateNode(id, label),
|
||||
label: (
|
||||
<Flex justify={'space-between'}>
|
||||
{t('common.copy')}
|
||||
<CopyOutlined />
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<OperateDropdown
|
||||
iconFontSize={22}
|
||||
height={14}
|
||||
deleteItem={deleteNode}
|
||||
items={items}
|
||||
needsDeletionValidation={false}
|
||||
iconFontColor={iconFontColor}
|
||||
></OperateDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeDropdown;
|
78
web/src/pages/agent/canvas/node/email-node.tsx
Normal file
78
web/src/pages/agent/canvas/node/email-node.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { IEmailNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function EmailNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IEmailNode>) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={classNames(styles.ragNode, {
|
||||
[styles.selectedNode]: selected,
|
||||
})}
|
||||
>
|
||||
<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}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
|
||||
<Flex vertical gap={8} className={styles.emailNodeContainer}>
|
||||
<div
|
||||
className={styles.emailConfig}
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>SMTP:</span>
|
||||
<span className={styles.configValue}>{data.form?.smtp_server}</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>Port:</span>
|
||||
<span className={styles.configValue}>{data.form?.smtp_port}</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>From:</span>
|
||||
<span className={styles.configValue}>{data.form?.email}</span>
|
||||
</div>
|
||||
<div className={styles.expandIcon}>{showDetails ? '▼' : '▶'}</div>
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<div className={styles.jsonExample}>
|
||||
<div className={styles.jsonTitle}>Expected Input JSON:</div>
|
||||
<pre className={styles.jsonContent}>
|
||||
{`{
|
||||
"to_email": "...",
|
||||
"cc_email": "...",
|
||||
"subject": "...",
|
||||
"content": "..."
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
57
web/src/pages/agent/canvas/node/generate-node.tsx
Normal file
57
web/src/pages/agent/canvas/node/generate-node.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IGenerateNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function GenerateNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IGenerateNode>) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<div className={styles.nodeText}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
20
web/src/pages/agent/canvas/node/handle-icon.tsx
Normal file
20
web/src/pages/agent/canvas/node/handle-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const HandleIcon = () => {
|
||||
return (
|
||||
<PlusOutlined
|
||||
style={{ fontSize: 6, color: 'white', position: 'absolute', zIndex: 10 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RightHandleStyle: CSSProperties = {
|
||||
right: 0,
|
||||
};
|
||||
|
||||
export const LeftHandleStyle: CSSProperties = {
|
||||
left: 0,
|
||||
};
|
||||
|
||||
export default HandleIcon;
|
104
web/src/pages/agent/canvas/node/hooks.ts
Normal file
104
web/src/pages/agent/canvas/node/hooks.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { useUpdateNodeInternals } from '@xyflow/react';
|
||||
import get from 'lodash/get';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { SwitchElseTo } from '../../constant';
|
||||
|
||||
import {
|
||||
ICategorizeItemResult,
|
||||
ISwitchCondition,
|
||||
RAGFlowNodeType,
|
||||
} from '@/interfaces/database/flow';
|
||||
import { generateSwitchHandleText } from '../../utils';
|
||||
|
||||
export const useBuildCategorizeHandlePositions = ({
|
||||
data,
|
||||
id,
|
||||
}: {
|
||||
id: string;
|
||||
data: RAGFlowNodeType['data'];
|
||||
}) => {
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
const categoryData: ICategorizeItemResult = useMemo(() => {
|
||||
return get(data, `form.category_description`, {});
|
||||
}, [data]);
|
||||
|
||||
const positions = useMemo(() => {
|
||||
const list: Array<{
|
||||
text: string;
|
||||
top: number;
|
||||
idx: number;
|
||||
}> = [];
|
||||
|
||||
Object.keys(categoryData)
|
||||
.sort((a, b) => categoryData[a].index - categoryData[b].index)
|
||||
.forEach((x, idx) => {
|
||||
list.push({
|
||||
text: x,
|
||||
idx,
|
||||
top: idx === 0 ? 98 + 20 : list[idx - 1].top + 8 + 26,
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
}, [categoryData]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [id, updateNodeInternals, categoryData]);
|
||||
|
||||
return { positions };
|
||||
};
|
||||
|
||||
export const useBuildSwitchHandlePositions = ({
|
||||
data,
|
||||
id,
|
||||
}: {
|
||||
id: string;
|
||||
data: RAGFlowNodeType['data'];
|
||||
}) => {
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
const conditions: ISwitchCondition[] = useMemo(() => {
|
||||
return get(data, 'form.conditions', []);
|
||||
}, [data]);
|
||||
|
||||
const positions = useMemo(() => {
|
||||
const list: Array<{
|
||||
text: string;
|
||||
top: number;
|
||||
idx: number;
|
||||
condition?: ISwitchCondition;
|
||||
}> = [];
|
||||
|
||||
[...conditions, ''].forEach((x, idx) => {
|
||||
let top = idx === 0 ? 58 + 20 : list[idx - 1].top + 32; // case number (Case 1) height + flex gap
|
||||
if (idx - 1 >= 0) {
|
||||
const previousItems = conditions[idx - 1]?.items ?? [];
|
||||
if (previousItems.length > 0) {
|
||||
top += 12; // ConditionBlock padding
|
||||
top += previousItems.length * 22; // condition variable height
|
||||
top += (previousItems.length - 1) * 25; // operator height
|
||||
}
|
||||
}
|
||||
|
||||
list.push({
|
||||
text:
|
||||
idx < conditions.length
|
||||
? generateSwitchHandleText(idx)
|
||||
: SwitchElseTo,
|
||||
idx,
|
||||
top,
|
||||
condition: typeof x === 'string' ? undefined : x,
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
}, [conditions]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [id, updateNodeInternals, conditions]);
|
||||
|
||||
return { positions };
|
||||
};
|
285
web/src/pages/agent/canvas/node/index.less
Normal file
285
web/src/pages/agent/canvas/node/index.less
Normal file
@ -0,0 +1,285 @@
|
||||
.dark {
|
||||
background: rgb(63, 63, 63) !important;
|
||||
}
|
||||
.ragNode {
|
||||
.commonNode();
|
||||
.nodeName {
|
||||
font-size: 10px;
|
||||
color: black;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
}
|
||||
.description {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.categorizeAnchorPointText {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@lightBackgroundColor: rgba(150, 150, 150, 0.1);
|
||||
@darkBackgroundColor: rgba(150, 150, 150, 0.2);
|
||||
|
||||
.selectedNode {
|
||||
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;
|
||||
justify-content: center;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: rgb(59, 88, 253);
|
||||
border: 1px solid white;
|
||||
z-index: 1;
|
||||
background-image: url('@/assets/svg/plus.svg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.jsonView {
|
||||
word-wrap: break-word;
|
||||
overflow: auto;
|
||||
max-width: 300px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.logicNode {
|
||||
.commonNode();
|
||||
|
||||
.nodeName {
|
||||
font-size: 10px;
|
||||
color: black;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.categorizeAnchorPointText {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.relevantSourceLabel {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.noteNode {
|
||||
.commonNode();
|
||||
min-width: 140px;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
min-height: 128px;
|
||||
.noteTitle {
|
||||
background-color: #edfcff;
|
||||
font-size: 12px;
|
||||
padding: 6px 6px 4px;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.noteTitleDark {
|
||||
background-color: #edfcff;
|
||||
font-size: 12px;
|
||||
padding: 6px 6px 4px;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.noteForm {
|
||||
margin-top: 4px;
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
.noteName {
|
||||
padding: 0px 4px;
|
||||
}
|
||||
.noteTextarea {
|
||||
resize: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
&:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iterationNode {
|
||||
.commonNodeShadow();
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.nodeText {
|
||||
padding-inline: 0.4em;
|
||||
padding-block: 0.2em 0.1em;
|
||||
background: @lightBackgroundColor;
|
||||
border-radius: 3px;
|
||||
min-height: 22px;
|
||||
.textEllipsis();
|
||||
}
|
||||
|
||||
.nodeHeader {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.zeroDivider {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.conditionBlock {
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
background: @lightBackgroundColor;
|
||||
}
|
||||
|
||||
.conditionLine {
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
background: @darkBackgroundColor;
|
||||
.textEllipsis();
|
||||
}
|
||||
|
||||
.conditionKey {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.conditionOperator {
|
||||
padding: 0 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.relevantLabel {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.knowledgeNodeName {
|
||||
.textEllipsis();
|
||||
}
|
||||
|
||||
.messageNodeContainer {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.generateParameters {
|
||||
padding-top: 8px;
|
||||
label {
|
||||
flex: 2;
|
||||
.textEllipsis();
|
||||
}
|
||||
.parameterValue {
|
||||
flex: 3;
|
||||
.conditionLine;
|
||||
}
|
||||
}
|
||||
|
||||
.emailNodeContainer {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
.emailConfig {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.configItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.configLabel {
|
||||
color: #666;
|
||||
width: 45px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.configValue {
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.jsonExample {
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin-top: 4px;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
|
||||
.jsonTitle {
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.jsonContent {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
45
web/src/pages/agent/canvas/node/index.tsx
Normal file
45
web/src/pages/agent/canvas/node/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function RagNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRagNode>) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.ragNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}></NodeHeader>
|
||||
</section>
|
||||
);
|
||||
}
|
59
web/src/pages/agent/canvas/node/invoke-node.tsx
Normal file
59
web/src/pages/agent/canvas/node/invoke-node.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IInvokeNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function InvokeNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IInvokeNode>) {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useTheme();
|
||||
const url = get(data, 'form.url');
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.ragNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
<Flex vertical>
|
||||
<div>{t('flow.url')}</div>
|
||||
<div className={styles.nodeText}>{url}</div>
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
127
web/src/pages/agent/canvas/node/iteration-node.tsx
Normal file
127
web/src/pages/agent/canvas/node/iteration-node.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import {
|
||||
IIterationNode,
|
||||
IIterationStartNode,
|
||||
} from '@/interfaces/database/flow';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Handle, NodeProps, NodeResizeControl, Position } from '@xyflow/react';
|
||||
import { ListRestart } from 'lucide-react';
|
||||
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',
|
||||
cursor: 'nwse-resize',
|
||||
};
|
||||
|
||||
export function IterationNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IIterationNode>) {
|
||||
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<IIterationStartNode>) {
|
||||
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}
|
||||
isConnectableEnd={false}
|
||||
></Handle>
|
||||
<div>
|
||||
<ListRestart className="size-7" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
57
web/src/pages/agent/canvas/node/keyword-node.tsx
Normal file
57
web/src/pages/agent/canvas/node/keyword-node.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IKeywordNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function KeywordNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IKeywordNode>) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<div className={styles.nodeText}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
45
web/src/pages/agent/canvas/node/logic-node.tsx
Normal file
45
web/src/pages/agent/canvas/node/logic-node.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { ILogicNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function LogicNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<ILogicNode>) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
</section>
|
||||
);
|
||||
}
|
65
web/src/pages/agent/canvas/node/message-node.tsx
Normal file
65
web/src/pages/agent/canvas/node/message-node.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IMessageNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function MessageNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IMessageNode>) {
|
||||
const messages: string[] = get(data, 'form.messages', []);
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={classNames({
|
||||
[styles.nodeHeader]: messages.length > 0,
|
||||
})}
|
||||
></NodeHeader>
|
||||
|
||||
<Flex vertical gap={8} className={styles.messageNodeContainer}>
|
||||
{messages.map((message, idx) => {
|
||||
return (
|
||||
<div className={styles.nodeText} key={idx}>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
73
web/src/pages/agent/canvas/node/node-header.tsx
Normal file
73
web/src/pages/agent/canvas/node/node-header.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { Flex } from 'antd';
|
||||
import { Play } from 'lucide-react';
|
||||
import { Operator, operatorMap } from '../../constant';
|
||||
import OperatorIcon from '../../operator-icon';
|
||||
import { needsSingleStepDebugging } from '../../utils';
|
||||
import NodeDropdown from './dropdown';
|
||||
import { NextNodePopover } from './popover';
|
||||
|
||||
import { RunTooltip } from '../../flow-tooltip';
|
||||
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 (
|
||||
<section className="flex justify-end items-center pb-1 gap-2 text-blue-600">
|
||||
{needsSingleStepDebugging(label) && (
|
||||
<RunTooltip>
|
||||
<Play className="size-3 cursor-pointer" data-play />
|
||||
</RunTooltip> // data-play is used to trigger single step debugging
|
||||
)}
|
||||
<NextNodePopover nodeId={id} name={name}>
|
||||
<span className="cursor-pointer text-[10px]">
|
||||
{t('operationResults')}
|
||||
</span>
|
||||
</NextNodePopover>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const NodeHeader = ({
|
||||
label,
|
||||
id,
|
||||
name,
|
||||
gap = 4,
|
||||
className,
|
||||
wrapperClassName,
|
||||
}: IProps) => {
|
||||
return (
|
||||
<section className={wrapperClassName}>
|
||||
{!ExcludedRunStateOperators.includes(label as Operator) && (
|
||||
<RunStatus id={id} name={name} label={label}></RunStatus>
|
||||
)}
|
||||
<Flex
|
||||
flex={1}
|
||||
align="center"
|
||||
justify={'space-between'}
|
||||
gap={gap}
|
||||
className={className}
|
||||
>
|
||||
<OperatorIcon
|
||||
name={label as Operator}
|
||||
color={operatorMap[label as Operator]?.color}
|
||||
></OperatorIcon>
|
||||
<span className="truncate text-center font-semibold text-sm">
|
||||
{name}
|
||||
</span>
|
||||
<NodeDropdown id={id} label={label}></NodeDropdown>
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeHeader;
|
92
web/src/pages/agent/canvas/node/note-node.tsx
Normal file
92
web/src/pages/agent/canvas/node/note-node.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { NodeProps, NodeResizeControl } from '@xyflow/react';
|
||||
import { Flex, Form, Input } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import NodeDropdown from './dropdown';
|
||||
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { INoteNode } from '@/interfaces/database/flow';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useHandleFormValuesChange,
|
||||
useHandleNodeNameChange,
|
||||
} from '../../hooks';
|
||||
import styles from './index.less';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const controlStyle = {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
};
|
||||
|
||||
function NoteNode({ data, id }: NodeProps<INoteNode>) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
|
||||
id,
|
||||
data,
|
||||
});
|
||||
const { handleValuesChange } = useHandleFormValuesChange(id);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(data?.form);
|
||||
}, [form, data?.form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeResizeControl style={controlStyle} minWidth={190} minHeight={128}>
|
||||
<SvgIcon
|
||||
name="resize"
|
||||
width={12}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
cursor: 'nwse-resize',
|
||||
}}
|
||||
></SvgIcon>
|
||||
</NodeResizeControl>
|
||||
<section
|
||||
className={classNames(
|
||||
styles.noteNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
)}
|
||||
>
|
||||
<Flex
|
||||
justify={'space-between'}
|
||||
className={classNames('note-drag-handle')}
|
||||
align="center"
|
||||
gap={6}
|
||||
>
|
||||
<SvgIcon name="note" width={14}></SvgIcon>
|
||||
<Input
|
||||
value={name ?? t('flow.note')}
|
||||
onBlur={handleNameBlur}
|
||||
onChange={handleNameChange}
|
||||
className={styles.noteName}
|
||||
></Input>
|
||||
<NodeDropdown id={id} label={data.label}></NodeDropdown>
|
||||
</Flex>
|
||||
<Form
|
||||
onValuesChange={handleValuesChange}
|
||||
form={form}
|
||||
className={styles.noteForm}
|
||||
>
|
||||
<Form.Item name="text" noStyle>
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder={t('flow.notePlaceholder')}
|
||||
className={styles.noteTextarea}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NoteNode);
|
121
web/src/pages/agent/canvas/node/popover.tsx
Normal file
121
web/src/pages/agent/canvas/node/popover.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { useFetchFlow } from '@/hooks/flow-hooks';
|
||||
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 { useReplaceIdWithText } from '../../hooks';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
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;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function NextNodePopover({ children, nodeId, name }: IProps) {
|
||||
const { t } = useTranslate('flow');
|
||||
|
||||
const { data } = useFetchFlow();
|
||||
const { theme } = useTheme();
|
||||
const component = useMemo(() => {
|
||||
return get(data, ['dsl', 'components', nodeId], {});
|
||||
}, [nodeId, data]);
|
||||
|
||||
const inputs: Array<{ component_id: string; content: string }> = get(
|
||||
component,
|
||||
['obj', 'inputs'],
|
||||
[],
|
||||
);
|
||||
const output = get(component, ['obj', 'output'], {});
|
||||
const { replacedOutput } = useReplaceIdWithText(output);
|
||||
const stopPropagation: MouseEventHandler = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const getLabel = useGetComponentLabelByValue(nodeId);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger onClick={stopPropagation} asChild>
|
||||
{children}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align={'start'}
|
||||
side={'right'}
|
||||
sideOffset={20}
|
||||
onClick={stopPropagation}
|
||||
className="w-[400px]"
|
||||
>
|
||||
<div className="mb-3 font-semibold text-[16px]">
|
||||
{name} {t('operationResults')}
|
||||
</div>
|
||||
<div className="flex w-full gap-4 flex-col">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<span className="font-semibold text-[14px]">{t('input')}</span>
|
||||
<div
|
||||
style={
|
||||
theme === 'dark'
|
||||
? {
|
||||
backgroundColor: 'rgba(150, 150, 150, 0.2)',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
className={`bg-gray-100 p-1 rounded`}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t('componentId')}</TableHead>
|
||||
<TableHead className="w-[60px]">{t('content')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{inputs.map((x, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell>{getLabel(x.component_id)}</TableCell>
|
||||
<TableCell className="truncate">{x.content}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<span className="font-semibold text-[14px]">{t('output')}</span>
|
||||
<div
|
||||
style={
|
||||
theme === 'dark'
|
||||
? {
|
||||
backgroundColor: 'rgba(150, 150, 150, 0.2)',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
className="bg-gray-100 p-1 rounded"
|
||||
>
|
||||
<JsonView
|
||||
src={replacedOutput}
|
||||
displaySize={30}
|
||||
className="w-full max-h-[300px] break-words overflow-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
70
web/src/pages/agent/canvas/node/relevant-node.tsx
Normal file
70
web/src/pages/agent/canvas/node/relevant-node.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IRelevantNode } from '@/interfaces/database/flow';
|
||||
import { get } from 'lodash';
|
||||
import { useReplaceIdWithName } from '../../hooks';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function RelevantNode({ id, data, selected }: NodeProps<IRelevantNode>) {
|
||||
const yes = get(data, 'form.yes');
|
||||
const no = get(data, 'form.no');
|
||||
const replaceIdWithName = useReplaceIdWithName();
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
id={'a'}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
id={'yes'}
|
||||
style={{ ...RightHandleStyle, top: 57 + 20 }}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
id={'no'}
|
||||
style={{ ...RightHandleStyle, top: 115 + 20 }}
|
||||
></Handle>
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<Flex vertical gap={10}>
|
||||
<Flex vertical>
|
||||
<div className={styles.relevantLabel}>Yes</div>
|
||||
<div className={styles.nodeText}>{replaceIdWithName(yes)}</div>
|
||||
</Flex>
|
||||
<Flex vertical>
|
||||
<div className={styles.relevantLabel}>No</div>
|
||||
<div className={styles.nodeText}>{replaceIdWithName(no)}</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
88
web/src/pages/agent/canvas/node/retrieval-node.tsx
Normal file
88
web/src/pages/agent/canvas/node/retrieval-node.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
||||
import { IRetrievalNode } from '@/interfaces/database/flow';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Avatar, Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function RetrievalNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRetrievalNode>) {
|
||||
const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []);
|
||||
const { theme } = useTheme();
|
||||
const { list: knowledgeList } = useFetchKnowledgeList(true);
|
||||
const knowledgeBases = useMemo(() => {
|
||||
return knowledgeBaseIds.map((x) => {
|
||||
const item = knowledgeList.find((y) => x === y.id);
|
||||
return {
|
||||
name: item?.name,
|
||||
avatar: item?.avatar,
|
||||
id: x,
|
||||
};
|
||||
});
|
||||
}, [knowledgeList, knowledgeBaseIds]);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={classNames({
|
||||
[styles.nodeHeader]: knowledgeBaseIds.length > 0,
|
||||
})}
|
||||
></NodeHeader>
|
||||
<Flex vertical gap={8}>
|
||||
{knowledgeBases.map((knowledge) => {
|
||||
return (
|
||||
<div className={styles.nodeText} key={knowledge.id}>
|
||||
<Flex align={'center'} gap={6}>
|
||||
<Avatar
|
||||
size={26}
|
||||
icon={<UserOutlined />}
|
||||
src={knowledge.avatar}
|
||||
/>
|
||||
<Flex className={styles.knowledgeNodeName} flex={1}>
|
||||
{knowledge.name}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
57
web/src/pages/agent/canvas/node/rewrite-node.tsx
Normal file
57
web/src/pages/agent/canvas/node/rewrite-node.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IRewriteNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function RewriteNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRewriteNode>) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<div className={styles.nodeText}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
114
web/src/pages/agent/canvas/node/switch-node.tsx
Normal file
114
web/src/pages/agent/canvas/node/switch-node.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Divider, Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
import { useBuildSwitchHandlePositions } from './hooks';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
const getConditionKey = (idx: number, length: number) => {
|
||||
if (idx === 0 && length !== 1) {
|
||||
return 'If';
|
||||
} else if (idx === length - 1) {
|
||||
return 'Else';
|
||||
}
|
||||
|
||||
return 'ElseIf';
|
||||
};
|
||||
|
||||
const ConditionBlock = ({
|
||||
condition,
|
||||
nodeId,
|
||||
}: {
|
||||
condition: ISwitchCondition;
|
||||
nodeId: string;
|
||||
}) => {
|
||||
const items = condition?.items ?? [];
|
||||
const getLabel = useGetComponentLabelByValue(nodeId);
|
||||
return (
|
||||
<Flex vertical className={styles.conditionBlock}>
|
||||
{items.map((x, idx) => (
|
||||
<div key={idx}>
|
||||
<Flex>
|
||||
<div
|
||||
className={classNames(styles.conditionLine, styles.conditionKey)}
|
||||
>
|
||||
{getLabel(x?.cpn_id)}
|
||||
</div>
|
||||
<span className={styles.conditionOperator}>{x?.operator}</span>
|
||||
<Flex flex={1} className={styles.conditionLine}>
|
||||
{x?.value}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{idx + 1 < items.length && (
|
||||
<Divider orientationMargin="0" className={styles.zeroDivider}>
|
||||
{condition?.logical_operator}
|
||||
</Divider>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export function SwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
|
||||
const { positions } = useBuildSwitchHandlePositions({ data, id });
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
id={'a'}
|
||||
></Handle>
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
<Flex vertical gap={10}>
|
||||
{positions.map((position, idx) => {
|
||||
return (
|
||||
<div key={idx}>
|
||||
<Flex vertical>
|
||||
<Flex justify={'space-between'}>
|
||||
<span>{idx < positions.length - 1 && position.text}</span>
|
||||
<span>{getConditionKey(idx, positions.length)}</span>
|
||||
</Flex>
|
||||
{position.condition && (
|
||||
<ConditionBlock
|
||||
nodeId={id}
|
||||
condition={position.condition}
|
||||
></ConditionBlock>
|
||||
)}
|
||||
</Flex>
|
||||
<Handle
|
||||
key={position.text}
|
||||
id={position.text}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
style={{ ...RightHandleStyle, top: position.top }}
|
||||
></Handle>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
75
web/src/pages/agent/canvas/node/template-node.tsx
Normal file
75
web/src/pages/agent/canvas/node/template-node.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
import { IGenerateParameter } from '../../interface';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
import { ITemplateNode } from '@/interfaces/database/flow';
|
||||
import styles from './index.less';
|
||||
|
||||
export function TemplateNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<ITemplateNode>) {
|
||||
const parameters: IGenerateParameter[] = get(data, 'form.parameters', []);
|
||||
const getLabel = useGetComponentLabelByValue(id);
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<Flex gap={8} vertical className={styles.generateParameters}>
|
||||
{parameters.map((x) => (
|
||||
<Flex
|
||||
key={x.id}
|
||||
align="center"
|
||||
gap={6}
|
||||
className={styles.conditionBlock}
|
||||
>
|
||||
<label htmlFor="">{x.key}</label>
|
||||
<span className={styles.parameterValue}>
|
||||
{getLabel(x.component_id)}
|
||||
</span>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
19
web/src/pages/agent/flow-tooltip.tsx
Normal file
19
web/src/pages/agent/flow-tooltip.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const RunTooltip = ({ children }: PropsWithChildren) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{children}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('flow.testRun')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
567
web/src/pages/agent/hooks.tsx
Normal file
567
web/src/pages/agent/hooks.tsx
Normal file
@ -0,0 +1,567 @@
|
||||
import {
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
Position,
|
||||
ReactFlowInstance,
|
||||
} from '@xyflow/react';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
// import { shallow } from 'zustand/shallow';
|
||||
import { settledModelVariableMap } from '@/constants/knowledge';
|
||||
import { useFetchModelId } from '@/hooks/logic-hooks';
|
||||
import {
|
||||
ICategorizeForm,
|
||||
IRelevantForm,
|
||||
ISwitchForm,
|
||||
RAGFlowNodeType,
|
||||
} from '@/interfaces/database/flow';
|
||||
import { message } from 'antd';
|
||||
import { humanId } from 'human-id';
|
||||
import { get, lowerFirst } from 'lodash';
|
||||
import trim from 'lodash/trim';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
NodeMap,
|
||||
Operator,
|
||||
RestrictedUpstreamMap,
|
||||
SwitchElseTo,
|
||||
initialAkShareValues,
|
||||
initialArXivValues,
|
||||
initialBaiduFanyiValues,
|
||||
initialBaiduValues,
|
||||
initialBeginValues,
|
||||
initialBingValues,
|
||||
initialCategorizeValues,
|
||||
initialConcentratorValues,
|
||||
initialCrawlerValues,
|
||||
initialDeepLValues,
|
||||
initialDuckValues,
|
||||
initialEmailValues,
|
||||
initialExeSqlValues,
|
||||
initialGenerateValues,
|
||||
initialGithubValues,
|
||||
initialGoogleScholarValues,
|
||||
initialGoogleValues,
|
||||
initialInvokeValues,
|
||||
initialIterationValues,
|
||||
initialJin10Values,
|
||||
initialKeywordExtractValues,
|
||||
initialMessageValues,
|
||||
initialNoteValues,
|
||||
initialPubMedValues,
|
||||
initialQWeatherValues,
|
||||
initialRelevantValues,
|
||||
initialRetrievalValues,
|
||||
initialRewriteQuestionValues,
|
||||
initialSwitchValues,
|
||||
initialTemplateValues,
|
||||
initialTuShareValues,
|
||||
initialWenCaiValues,
|
||||
initialWikipediaValues,
|
||||
initialYahooFinanceValues,
|
||||
} from './constant';
|
||||
import useGraphStore, { RFState } from './store';
|
||||
import {
|
||||
generateNodeNamesWithIncreasingIndex,
|
||||
generateSwitchHandleText,
|
||||
getNodeDragHandle,
|
||||
getRelativePositionToIterationNode,
|
||||
replaceIdWithText,
|
||||
} from './utils';
|
||||
|
||||
const selector = (state: RFState) => ({
|
||||
nodes: state.nodes,
|
||||
edges: state.edges,
|
||||
onNodesChange: state.onNodesChange,
|
||||
onEdgesChange: state.onEdgesChange,
|
||||
onConnect: state.onConnect,
|
||||
setNodes: state.setNodes,
|
||||
onSelectionChange: state.onSelectionChange,
|
||||
});
|
||||
|
||||
export const useSelectCanvasData = () => {
|
||||
// return useStore(useShallow(selector)); // throw error
|
||||
// return useStore(selector, shallow);
|
||||
return useGraphStore(selector);
|
||||
};
|
||||
|
||||
export const useInitializeOperatorParams = () => {
|
||||
const llmId = useFetchModelId();
|
||||
|
||||
const initialFormValuesMap = useMemo(() => {
|
||||
return {
|
||||
[Operator.Begin]: initialBeginValues,
|
||||
[Operator.Retrieval]: initialRetrievalValues,
|
||||
[Operator.Generate]: { ...initialGenerateValues, llm_id: llmId },
|
||||
[Operator.Answer]: {},
|
||||
[Operator.Categorize]: { ...initialCategorizeValues, llm_id: llmId },
|
||||
[Operator.Relevant]: { ...initialRelevantValues, llm_id: llmId },
|
||||
[Operator.RewriteQuestion]: {
|
||||
...initialRewriteQuestionValues,
|
||||
llm_id: llmId,
|
||||
},
|
||||
[Operator.Message]: initialMessageValues,
|
||||
[Operator.KeywordExtract]: {
|
||||
...initialKeywordExtractValues,
|
||||
llm_id: llmId,
|
||||
},
|
||||
[Operator.DuckDuckGo]: initialDuckValues,
|
||||
[Operator.Baidu]: initialBaiduValues,
|
||||
[Operator.Wikipedia]: initialWikipediaValues,
|
||||
[Operator.PubMed]: initialPubMedValues,
|
||||
[Operator.ArXiv]: initialArXivValues,
|
||||
[Operator.Google]: initialGoogleValues,
|
||||
[Operator.Bing]: initialBingValues,
|
||||
[Operator.GoogleScholar]: initialGoogleScholarValues,
|
||||
[Operator.DeepL]: initialDeepLValues,
|
||||
[Operator.GitHub]: initialGithubValues,
|
||||
[Operator.BaiduFanyi]: initialBaiduFanyiValues,
|
||||
[Operator.QWeather]: initialQWeatherValues,
|
||||
[Operator.ExeSQL]: { ...initialExeSqlValues, llm_id: llmId },
|
||||
[Operator.Switch]: initialSwitchValues,
|
||||
[Operator.WenCai]: initialWenCaiValues,
|
||||
[Operator.AkShare]: initialAkShareValues,
|
||||
[Operator.YahooFinance]: initialYahooFinanceValues,
|
||||
[Operator.Jin10]: initialJin10Values,
|
||||
[Operator.Concentrator]: initialConcentratorValues,
|
||||
[Operator.TuShare]: initialTuShareValues,
|
||||
[Operator.Note]: initialNoteValues,
|
||||
[Operator.Crawler]: initialCrawlerValues,
|
||||
[Operator.Invoke]: initialInvokeValues,
|
||||
[Operator.Template]: initialTemplateValues,
|
||||
[Operator.Email]: initialEmailValues,
|
||||
[Operator.Iteration]: initialIterationValues,
|
||||
[Operator.IterationStart]: initialIterationValues,
|
||||
};
|
||||
}, [llmId]);
|
||||
|
||||
const initializeOperatorParams = useCallback(
|
||||
(operatorName: Operator) => {
|
||||
return initialFormValuesMap[operatorName];
|
||||
},
|
||||
[initialFormValuesMap],
|
||||
);
|
||||
|
||||
return initializeOperatorParams;
|
||||
};
|
||||
|
||||
export const useHandleDrag = () => {
|
||||
const handleDragStart = useCallback(
|
||||
(operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => {
|
||||
ev.dataTransfer.setData('application/@xyflow/react', operatorId);
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { handleDragStart };
|
||||
};
|
||||
|
||||
export const useGetNodeName = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (type: string) => {
|
||||
const name = t(`flow.${lowerFirst(type)}`);
|
||||
return name;
|
||||
};
|
||||
};
|
||||
|
||||
export const useHandleDrop = () => {
|
||||
const addNode = useGraphStore((state) => state.addNode);
|
||||
const nodes = useGraphStore((state) => state.nodes);
|
||||
const [reactFlowInstance, setReactFlowInstance] =
|
||||
useState<ReactFlowInstance<any, any>>();
|
||||
const initializeOperatorParams = useInitializeOperatorParams();
|
||||
const getNodeName = useGetNodeName();
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const type = event.dataTransfer.getData('application/@xyflow/react');
|
||||
|
||||
// check if the dropped element is valid
|
||||
if (typeof type === 'undefined' || !type) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
|
||||
// and you don't need to subtract the reactFlowBounds.left/top anymore
|
||||
// details: https://@xyflow/react.dev/whats-new/2023-11-10
|
||||
const position = reactFlowInstance?.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
const newNode: Node<any> = {
|
||||
id: `${type}:${humanId()}`,
|
||||
type: NodeMap[type as Operator] || 'ragNode',
|
||||
position: position || {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
data: {
|
||||
label: `${type}`,
|
||||
name: generateNodeNamesWithIncreasingIndex(getNodeName(type), nodes),
|
||||
form: initializeOperatorParams(type as Operator),
|
||||
},
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
dragHandle: getNodeDragHandle(type),
|
||||
};
|
||||
|
||||
if (type === Operator.Iteration) {
|
||||
newNode.width = 500;
|
||||
newNode.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],
|
||||
);
|
||||
|
||||
return { onDrop, onDragOver, setReactFlowInstance };
|
||||
};
|
||||
|
||||
export const useHandleFormValuesChange = (id?: string) => {
|
||||
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||
const handleValuesChange = useCallback(
|
||||
(changedValues: any, values: any) => {
|
||||
let nextValues: any = values;
|
||||
// Fixed the issue that the related form value does not change after selecting the freedom field of the model
|
||||
if (
|
||||
Object.keys(changedValues).length === 1 &&
|
||||
'parameter' in changedValues &&
|
||||
changedValues['parameter'] in settledModelVariableMap
|
||||
) {
|
||||
nextValues = {
|
||||
...values,
|
||||
...settledModelVariableMap[
|
||||
changedValues['parameter'] as keyof typeof settledModelVariableMap
|
||||
],
|
||||
};
|
||||
}
|
||||
if (id) {
|
||||
updateNodeForm(id, nextValues);
|
||||
}
|
||||
},
|
||||
[updateNodeForm, id],
|
||||
);
|
||||
|
||||
return { handleValuesChange };
|
||||
};
|
||||
|
||||
export const useValidateConnection = () => {
|
||||
const { edges, getOperatorTypeFromId, getParentIdById } = useGraphStore(
|
||||
(state) => state,
|
||||
);
|
||||
|
||||
const isSameNodeChild = useCallback(
|
||||
(connection: Connection | Edge) => {
|
||||
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 | Edge) => {
|
||||
// node cannot connect to itself
|
||||
const isSelfConnected = connection.target === connection.source;
|
||||
|
||||
// limit the connection between two nodes to only one connection line in one direction
|
||||
const hasLine = edges.some(
|
||||
(x) => x.source === connection.source && x.target === connection.target,
|
||||
);
|
||||
|
||||
const ret =
|
||||
!isSelfConnected &&
|
||||
!hasLine &&
|
||||
RestrictedUpstreamMap[
|
||||
getOperatorTypeFromId(connection.source) as Operator
|
||||
]?.every((x) => x !== getOperatorTypeFromId(connection.target)) &&
|
||||
isSameNodeChild(connection);
|
||||
return ret;
|
||||
},
|
||||
[edges, getOperatorTypeFromId, isSameNodeChild],
|
||||
);
|
||||
|
||||
return isValidConnection;
|
||||
};
|
||||
|
||||
export const useHandleNodeNameChange = ({
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
id?: string;
|
||||
data: any;
|
||||
}) => {
|
||||
const [name, setName] = useState<string>('');
|
||||
const { updateNodeName, nodes } = useGraphStore((state) => state);
|
||||
const previousName = data?.name;
|
||||
|
||||
const handleNameBlur = useCallback(() => {
|
||||
const existsSameName = nodes.some((x) => x.data.name === name);
|
||||
if (trim(name) === '' || existsSameName) {
|
||||
if (existsSameName && previousName !== name) {
|
||||
message.error('The name cannot be repeated');
|
||||
}
|
||||
setName(previousName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
updateNodeName(id, name);
|
||||
}
|
||||
}, [name, id, updateNodeName, previousName, nodes]);
|
||||
|
||||
const handleNameChange = useCallback((e: ChangeEvent<any>) => {
|
||||
setName(e.target.value);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setName(previousName);
|
||||
}, [previousName]);
|
||||
|
||||
return { name, handleNameBlur, handleNameChange };
|
||||
};
|
||||
|
||||
export const useReplaceIdWithName = () => {
|
||||
const getNode = useGraphStore((state) => state.getNode);
|
||||
|
||||
const replaceIdWithName = useCallback(
|
||||
(id?: string) => {
|
||||
return getNode(id)?.data.name;
|
||||
},
|
||||
[getNode],
|
||||
);
|
||||
|
||||
return replaceIdWithName;
|
||||
};
|
||||
|
||||
export const useReplaceIdWithText = (output: unknown) => {
|
||||
const getNameById = useReplaceIdWithName();
|
||||
|
||||
return {
|
||||
replacedOutput: replaceIdWithText(output, getNameById),
|
||||
getNameById,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* monitor changes in the data.form field of the categorize and relevant operators
|
||||
* and then synchronize them to the edge
|
||||
*/
|
||||
export const useWatchNodeFormDataChange = () => {
|
||||
const { getNode, nodes, setEdgesByNodeId } = useGraphStore((state) => state);
|
||||
|
||||
const buildCategorizeEdgesByFormData = useCallback(
|
||||
(nodeId: string, form: ICategorizeForm) => {
|
||||
// add
|
||||
// delete
|
||||
// edit
|
||||
const categoryDescription = form.category_description;
|
||||
const downstreamEdges = Object.keys(categoryDescription).reduce<Edge[]>(
|
||||
(pre, sourceHandle) => {
|
||||
const target = categoryDescription[sourceHandle]?.to;
|
||||
if (target) {
|
||||
pre.push({
|
||||
id: uuid(),
|
||||
source: nodeId,
|
||||
target,
|
||||
sourceHandle,
|
||||
});
|
||||
}
|
||||
|
||||
return pre;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
setEdgesByNodeId(nodeId, downstreamEdges);
|
||||
},
|
||||
[setEdgesByNodeId],
|
||||
);
|
||||
|
||||
const buildRelevantEdgesByFormData = useCallback(
|
||||
(nodeId: string, form: IRelevantForm) => {
|
||||
const downstreamEdges = ['yes', 'no'].reduce<Edge[]>((pre, cur) => {
|
||||
const target = form[cur as keyof IRelevantForm] as string;
|
||||
if (target) {
|
||||
pre.push({ id: uuid(), source: nodeId, target, sourceHandle: cur });
|
||||
}
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
|
||||
setEdgesByNodeId(nodeId, downstreamEdges);
|
||||
},
|
||||
[setEdgesByNodeId],
|
||||
);
|
||||
|
||||
const buildSwitchEdgesByFormData = useCallback(
|
||||
(nodeId: string, form: ISwitchForm) => {
|
||||
// add
|
||||
// delete
|
||||
// edit
|
||||
const conditions = form.conditions;
|
||||
const downstreamEdges = conditions.reduce<Edge[]>((pre, _, idx) => {
|
||||
const target = conditions[idx]?.to;
|
||||
if (target) {
|
||||
pre.push({
|
||||
id: uuid(),
|
||||
source: nodeId,
|
||||
target,
|
||||
sourceHandle: generateSwitchHandleText(idx),
|
||||
});
|
||||
}
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
|
||||
// Splice the else condition of the conditional judgment to the edge list
|
||||
const elseTo = form[SwitchElseTo];
|
||||
if (elseTo) {
|
||||
downstreamEdges.push({
|
||||
id: uuid(),
|
||||
source: nodeId,
|
||||
target: elseTo,
|
||||
sourceHandle: SwitchElseTo,
|
||||
});
|
||||
}
|
||||
|
||||
setEdgesByNodeId(nodeId, downstreamEdges);
|
||||
},
|
||||
[setEdgesByNodeId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
nodes.forEach((node) => {
|
||||
const currentNode = getNode(node.id);
|
||||
const form = currentNode?.data.form ?? {};
|
||||
const operatorType = currentNode?.data.label;
|
||||
switch (operatorType) {
|
||||
case Operator.Relevant:
|
||||
buildRelevantEdgesByFormData(node.id, form as IRelevantForm);
|
||||
break;
|
||||
case Operator.Categorize:
|
||||
buildCategorizeEdgesByFormData(node.id, form as ICategorizeForm);
|
||||
break;
|
||||
case Operator.Switch:
|
||||
buildSwitchEdgesByFormData(node.id, form as ISwitchForm);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}, [
|
||||
nodes,
|
||||
buildCategorizeEdgesByFormData,
|
||||
getNode,
|
||||
buildRelevantEdgesByFormData,
|
||||
buildSwitchEdgesByFormData,
|
||||
]);
|
||||
};
|
||||
|
||||
export const useDuplicateNode = () => {
|
||||
const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
|
||||
const getNodeName = useGetNodeName();
|
||||
|
||||
const duplicateNode = useCallback(
|
||||
(id: string, label: string) => {
|
||||
duplicateNodeById(id, getNodeName(label));
|
||||
},
|
||||
[duplicateNodeById, getNodeName],
|
||||
);
|
||||
|
||||
return duplicateNode;
|
||||
};
|
||||
|
||||
export const useCopyPaste = () => {
|
||||
const nodes = useGraphStore((state) => state.nodes);
|
||||
const duplicateNode = useDuplicateNode();
|
||||
|
||||
const onCopyCapture = useCallback(
|
||||
(event: ClipboardEvent) => {
|
||||
if (get(event, 'srcElement.tagName') !== 'BODY') return;
|
||||
|
||||
event.preventDefault();
|
||||
const nodesStr = JSON.stringify(
|
||||
nodes.filter((n) => n.selected && n.data.label !== Operator.Begin),
|
||||
);
|
||||
|
||||
event.clipboardData?.setData('agent:nodes', nodesStr);
|
||||
},
|
||||
[nodes],
|
||||
);
|
||||
|
||||
const onPasteCapture = useCallback(
|
||||
(event: ClipboardEvent) => {
|
||||
const nodes = JSON.parse(
|
||||
event.clipboardData?.getData('agent:nodes') || '[]',
|
||||
) as RAGFlowNodeType[] | undefined;
|
||||
|
||||
if (Array.isArray(nodes) && nodes.length) {
|
||||
event.preventDefault();
|
||||
nodes.forEach((n) => {
|
||||
duplicateNode(n.id, n.data.label);
|
||||
});
|
||||
}
|
||||
},
|
||||
[duplicateNode],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('copy', onCopyCapture);
|
||||
return () => {
|
||||
window.removeEventListener('copy', onCopyCapture);
|
||||
};
|
||||
}, [onCopyCapture]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('paste', onPasteCapture);
|
||||
return () => {
|
||||
window.removeEventListener('paste', onPasteCapture);
|
||||
};
|
||||
}, [onPasteCapture]);
|
||||
};
|
57
web/src/pages/agent/hooks/use-before-delete.tsx
Normal file
57
web/src/pages/agent/hooks/use-before-delete.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { OnBeforeDelete } from '@xyflow/react';
|
||||
import { Operator } from '../constant';
|
||||
import useGraphStore from '../store';
|
||||
|
||||
const UndeletableNodes = [Operator.Begin, Operator.IterationStart];
|
||||
|
||||
export function useBeforeDelete() {
|
||||
const getOperatorTypeFromId = useGraphStore(
|
||||
(state) => state.getOperatorTypeFromId,
|
||||
);
|
||||
const handleBeforeDelete: OnBeforeDelete<RAGFlowNodeType> = async ({
|
||||
nodes, // Nodes to be deleted
|
||||
edges, // Edges to be deleted
|
||||
}) => {
|
||||
const toBeDeletedNodes = nodes.filter((node) => {
|
||||
const operatorType = node.data?.label as Operator;
|
||||
if (operatorType === Operator.Begin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
operatorType === Operator.IterationStart &&
|
||||
!nodes.some((x) => x.id === node.parentId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const toBeDeletedEdges = edges.filter((edge) => {
|
||||
const sourceType = getOperatorTypeFromId(edge.source) as Operator;
|
||||
const downStreamNodes = nodes.filter((x) => x.id === edge.target);
|
||||
|
||||
// This edge does not need to be deleted, the range of edges that do not need to be deleted is smaller, so consider the case where it does not need to be deleted
|
||||
if (
|
||||
UndeletableNodes.includes(sourceType) && // Upstream node is Begin or IterationStart
|
||||
downStreamNodes.length === 0 // Downstream node does not exist in the nodes to be deleted
|
||||
) {
|
||||
if (!nodes.some((x) => x.id === edge.source)) {
|
||||
return true; // Can be deleted
|
||||
}
|
||||
return false; // Cannot be deleted
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: toBeDeletedNodes,
|
||||
edges: toBeDeletedEdges,
|
||||
};
|
||||
};
|
||||
|
||||
return { handleBeforeDelete };
|
||||
}
|
29
web/src/pages/agent/hooks/use-build-dsl.ts
Normal file
29
web/src/pages/agent/hooks/use-build-dsl.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { useFetchFlow } from '@/hooks/flow-hooks';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { useCallback } from 'react';
|
||||
import useGraphStore from '../store';
|
||||
import { buildDslComponentsByGraph } from '../utils';
|
||||
|
||||
export const useBuildDslData = () => {
|
||||
const { data } = useFetchFlow();
|
||||
const { nodes, edges } = useGraphStore((state) => state);
|
||||
|
||||
const buildDslData = useCallback(
|
||||
(currentNodes?: RAGFlowNodeType[]) => {
|
||||
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/agent/hooks/use-export-json.ts
Normal file
62
web/src/pages/agent/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/agent/hooks/use-fetch-data.ts
Normal file
19
web/src/pages/agent/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 };
|
||||
};
|
113
web/src/pages/agent/hooks/use-get-begin-query.tsx
Normal file
113
web/src/pages/agent/hooks/use-get-begin-query.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import get from 'lodash/get';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { BeginId, Operator } from '../constant';
|
||||
import { BeginQuery } 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 useGetBeginNodeDataQueryIsSafe = () => {
|
||||
const [isBeginNodeDataQuerySafe, setIsBeginNodeDataQuerySafe] =
|
||||
useState(false);
|
||||
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
|
||||
const nodes = useGraphStore((state) => state.nodes);
|
||||
|
||||
useEffect(() => {
|
||||
const query: BeginQuery[] = getBeginNodeDataQuery();
|
||||
const isSafe = !query.some((q) => !q.optional && q.type === 'file');
|
||||
setIsBeginNodeDataQuerySafe(isSafe);
|
||||
}, [getBeginNodeDataQuery, nodes]);
|
||||
|
||||
return isBeginNodeDataQuerySafe;
|
||||
};
|
||||
|
||||
// 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: RAGFlowNodeType) => {
|
||||
// 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/agent/hooks/use-iteration.ts
Normal file
0
web/src/pages/agent/hooks/use-iteration.ts
Normal file
12
web/src/pages/agent/hooks/use-open-document.ts
Normal file
12
web/src/pages/agent/hooks/use-open-document.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export function useOpenDocument() {
|
||||
const openDocument = useCallback(() => {
|
||||
window.open(
|
||||
'https://ragflow.io/docs/dev/category/agent-components',
|
||||
'_blank',
|
||||
);
|
||||
}, []);
|
||||
|
||||
return openDocument;
|
||||
}
|
85
web/src/pages/agent/hooks/use-save-graph.ts
Normal file
85
web/src/pages/agent/hooks/use-save-graph.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { useFetchFlow, useResetFlow, useSetFlow } from '@/hooks/flow-hooks';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
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?: RAGFlowNodeType[]) => {
|
||||
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?: RAGFlowNodeType[]) => {
|
||||
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/agent/hooks/use-set-graph.ts
Normal file
17
web/src/pages/agent/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/agent/hooks/use-show-drawer.tsx
Normal file
153
web/src/pages/agent/hooks/use-show-drawer.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { Node, NodeMouseHandler } from '@xyflow/react';
|
||||
import get from 'lodash/get';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
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,
|
||||
};
|
||||
}
|
@ -1,12 +1,22 @@
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { AgentSidebar } from './agent-sidebar';
|
||||
import FlowCanvas from './canvas';
|
||||
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
||||
|
||||
export default function Agent() {
|
||||
const { navigateToAgentList } = useNavigatePage();
|
||||
const {
|
||||
visible: chatDrawerVisible,
|
||||
hideModal: hideChatDrawer,
|
||||
showModal: showChatDrawer,
|
||||
} = useSetModalState();
|
||||
|
||||
useFetchDataOnMount();
|
||||
|
||||
return (
|
||||
<section>
|
||||
@ -33,7 +43,15 @@ export default function Agent() {
|
||||
<div>
|
||||
<SidebarProvider>
|
||||
<AgentSidebar />
|
||||
<div className="w-full">
|
||||
<SidebarTrigger />
|
||||
<div className="w-full h-full">
|
||||
<FlowCanvas
|
||||
drawerVisible={chatDrawerVisible}
|
||||
hideDrawer={hideChatDrawer}
|
||||
></FlowCanvas>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</section>
|
||||
|
30
web/src/pages/agent/interface.ts
Normal file
30
web/src/pages/agent/interface.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { FormInstance } from 'antd';
|
||||
|
||||
export interface IOperatorForm {
|
||||
onValuesChange?(changedValues: any, values: any): void;
|
||||
form?: FormInstance;
|
||||
node?: RAGFlowNodeType;
|
||||
nodeId?: string;
|
||||
}
|
||||
|
||||
export interface IGenerateParameter {
|
||||
id?: string;
|
||||
key: string;
|
||||
component_id?: string;
|
||||
}
|
||||
|
||||
export interface IInvokeVariable extends IGenerateParameter {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export type IPosition = { top: number; right: number; idx: number };
|
||||
|
||||
export interface BeginQuery {
|
||||
key: string;
|
||||
type: string;
|
||||
value: string;
|
||||
optional: boolean;
|
||||
name: string;
|
||||
options: (number | string | boolean)[];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Operator, operatorIconMap } from '../constant';
|
||||
import { Operator, operatorIconMap } from './constant';
|
||||
|
||||
interface IProps {
|
||||
name: Operator;
|
455
web/src/pages/agent/store.ts
Normal file
455
web/src/pages/agent/store.ts
Normal file
@ -0,0 +1,455 @@
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import type {} from '@redux-devtools/extension';
|
||||
import {
|
||||
Connection,
|
||||
Edge,
|
||||
EdgeChange,
|
||||
OnConnect,
|
||||
OnEdgesChange,
|
||||
OnNodesChange,
|
||||
OnSelectionChangeFunc,
|
||||
OnSelectionChangeParams,
|
||||
addEdge,
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
} from '@xyflow/react';
|
||||
import { omit } from 'lodash';
|
||||
import differenceWith from 'lodash/differenceWith';
|
||||
import intersectionWith from 'lodash/intersectionWith';
|
||||
import lodashSet from 'lodash/set';
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { Operator, SwitchElseTo } from './constant';
|
||||
import {
|
||||
duplicateNodeForm,
|
||||
generateDuplicateNode,
|
||||
generateNodeNamesWithIncreasingIndex,
|
||||
getOperatorIndex,
|
||||
isEdgeEqual,
|
||||
} from './utils';
|
||||
|
||||
export type RFState = {
|
||||
nodes: RAGFlowNodeType[];
|
||||
edges: Edge[];
|
||||
selectedNodeIds: string[];
|
||||
selectedEdgeIds: string[];
|
||||
clickedNodeId: string; // currently selected node
|
||||
onNodesChange: OnNodesChange<RAGFlowNodeType>;
|
||||
onEdgesChange: OnEdgesChange;
|
||||
onConnect: OnConnect;
|
||||
setNodes: (nodes: RAGFlowNodeType[]) => void;
|
||||
setEdges: (edges: Edge[]) => void;
|
||||
setEdgesByNodeId: (nodeId: string, edges: Edge[]) => void;
|
||||
updateNodeForm: (
|
||||
nodeId: string,
|
||||
values: any,
|
||||
path?: (string | number)[],
|
||||
) => RAGFlowNodeType[];
|
||||
onSelectionChange: OnSelectionChangeFunc;
|
||||
addNode: (nodes: RAGFlowNodeType) => void;
|
||||
getNode: (id?: string | null) => RAGFlowNodeType | undefined;
|
||||
addEdge: (connection: Connection) => void;
|
||||
getEdge: (id: string) => Edge | undefined;
|
||||
updateFormDataOnConnect: (connection: Connection) => void;
|
||||
updateSwitchFormData: (
|
||||
source: string,
|
||||
sourceHandle?: string | null,
|
||||
target?: string | null,
|
||||
) => 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) => RAGFlowNodeType | 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;
|
||||
};
|
||||
|
||||
// this is our useStore hook that we can use in our components to get parts of the store and call actions
|
||||
const useGraphStore = create<RFState>()(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
nodes: [] as RAGFlowNodeType[],
|
||||
edges: [] as Edge[],
|
||||
selectedNodeIds: [] as string[],
|
||||
selectedEdgeIds: [] as string[],
|
||||
clickedNodeId: '',
|
||||
onNodesChange: (changes) => {
|
||||
set({
|
||||
nodes: applyNodeChanges(changes, get().nodes),
|
||||
});
|
||||
},
|
||||
onEdgesChange: (changes: EdgeChange[]) => {
|
||||
set({
|
||||
edges: applyEdgeChanges(changes, get().edges),
|
||||
});
|
||||
},
|
||||
onConnect: (connection: Connection) => {
|
||||
const {
|
||||
deletePreviousEdgeOfClassificationNode,
|
||||
updateFormDataOnConnect,
|
||||
} = get();
|
||||
set({
|
||||
edges: addEdge(connection, get().edges),
|
||||
});
|
||||
deletePreviousEdgeOfClassificationNode(connection);
|
||||
updateFormDataOnConnect(connection);
|
||||
},
|
||||
onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => {
|
||||
set({
|
||||
selectedEdgeIds: edges.map((x) => x.id),
|
||||
selectedNodeIds: nodes.map((x) => x.id),
|
||||
});
|
||||
},
|
||||
setNodes: (nodes: RAGFlowNodeType[]) => {
|
||||
set({ nodes });
|
||||
},
|
||||
setEdges: (edges: Edge[]) => {
|
||||
set({ edges });
|
||||
},
|
||||
setEdgesByNodeId: (nodeId: string, currentDownstreamEdges: Edge[]) => {
|
||||
const { edges, setEdges } = get();
|
||||
// the previous downstream edge of this node
|
||||
const previousDownstreamEdges = edges.filter(
|
||||
(x) => x.source === nodeId,
|
||||
);
|
||||
const isDifferent =
|
||||
previousDownstreamEdges.length !== currentDownstreamEdges.length ||
|
||||
!previousDownstreamEdges.every((x) =>
|
||||
currentDownstreamEdges.some(
|
||||
(y) =>
|
||||
y.source === x.source &&
|
||||
y.target === x.target &&
|
||||
y.sourceHandle === x.sourceHandle,
|
||||
),
|
||||
) ||
|
||||
!currentDownstreamEdges.every((x) =>
|
||||
previousDownstreamEdges.some(
|
||||
(y) =>
|
||||
y.source === x.source &&
|
||||
y.target === x.target &&
|
||||
y.sourceHandle === x.sourceHandle,
|
||||
),
|
||||
);
|
||||
|
||||
const intersectionDownstreamEdges = intersectionWith(
|
||||
previousDownstreamEdges,
|
||||
currentDownstreamEdges,
|
||||
isEdgeEqual,
|
||||
);
|
||||
if (isDifferent) {
|
||||
// other operator's edges
|
||||
const irrelevantEdges = edges.filter((x) => x.source !== nodeId);
|
||||
// the added downstream edges
|
||||
const selfAddedDownstreamEdges = differenceWith(
|
||||
currentDownstreamEdges,
|
||||
intersectionDownstreamEdges,
|
||||
isEdgeEqual,
|
||||
);
|
||||
setEdges([
|
||||
...irrelevantEdges,
|
||||
...intersectionDownstreamEdges,
|
||||
...selfAddedDownstreamEdges,
|
||||
]);
|
||||
}
|
||||
},
|
||||
addNode: (node: RAGFlowNodeType) => {
|
||||
set({ nodes: get().nodes.concat(node) });
|
||||
},
|
||||
getNode: (id?: string | null) => {
|
||||
return get().nodes.find((x) => x.id === id);
|
||||
},
|
||||
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),
|
||||
});
|
||||
get().deletePreviousEdgeOfClassificationNode(connection);
|
||||
// TODO: This may not be reasonable. You need to choose between listening to changes in the form.
|
||||
get().updateFormDataOnConnect(connection);
|
||||
},
|
||||
getEdge: (id: string) => {
|
||||
return get().edges.find((x) => x.id === id);
|
||||
},
|
||||
updateFormDataOnConnect: (connection: Connection) => {
|
||||
const { getOperatorTypeFromId, updateNodeForm, updateSwitchFormData } =
|
||||
get();
|
||||
const { source, target, sourceHandle } = connection;
|
||||
const operatorType = getOperatorTypeFromId(source);
|
||||
if (source) {
|
||||
switch (operatorType) {
|
||||
case Operator.Relevant:
|
||||
updateNodeForm(source, { [sourceHandle as string]: target });
|
||||
break;
|
||||
case Operator.Categorize:
|
||||
if (sourceHandle)
|
||||
updateNodeForm(source, target, [
|
||||
'category_description',
|
||||
sourceHandle,
|
||||
'to',
|
||||
]);
|
||||
break;
|
||||
case Operator.Switch: {
|
||||
updateSwitchFormData(source, sourceHandle, target);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
deletePreviousEdgeOfClassificationNode: (connection: Connection) => {
|
||||
// Delete the edge on the classification node or relevant node anchor when the anchor is connected to other nodes
|
||||
const { edges, getOperatorTypeFromId, deleteEdgeById } = get();
|
||||
// the node containing the anchor
|
||||
const anchoredNodes = [
|
||||
Operator.Categorize,
|
||||
Operator.Relevant,
|
||||
Operator.Switch,
|
||||
];
|
||||
if (
|
||||
anchoredNodes.some(
|
||||
(x) => x === getOperatorTypeFromId(connection.source),
|
||||
)
|
||||
) {
|
||||
const previousEdge = edges.find(
|
||||
(x) =>
|
||||
x.source === connection.source &&
|
||||
x.sourceHandle === connection.sourceHandle &&
|
||||
x.target !== connection.target,
|
||||
);
|
||||
if (previousEdge) {
|
||||
deleteEdgeById(previousEdge.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
duplicateNode: (id: string, name: string) => {
|
||||
const { getNode, addNode, generateNodeName, duplicateIterationNode } =
|
||||
get();
|
||||
const node = getNode(id);
|
||||
|
||||
if (node?.data.label === Operator.Iteration) {
|
||||
duplicateIterationNode(id, name);
|
||||
return;
|
||||
}
|
||||
|
||||
addNode({
|
||||
...(node || {}),
|
||||
data: {
|
||||
...duplicateNodeForm(node?.data),
|
||||
name: generateNodeName(name),
|
||||
},
|
||||
...generateDuplicateNode(node?.position, node?.data?.label),
|
||||
});
|
||||
},
|
||||
duplicateIterationNode: (id: string, name: string) => {
|
||||
const { getNode, generateNodeName, nodes } = get();
|
||||
const node = getNode(id);
|
||||
|
||||
const iterationNode: RAGFlowNodeType = {
|
||||
...(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({
|
||||
edges: edges.filter((edge) =>
|
||||
selectedEdgeIds.every((x) => x !== edge.id),
|
||||
),
|
||||
});
|
||||
},
|
||||
deleteEdgeById: (id: string) => {
|
||||
const {
|
||||
edges,
|
||||
updateNodeForm,
|
||||
getOperatorTypeFromId,
|
||||
updateSwitchFormData,
|
||||
} = get();
|
||||
const currentEdge = edges.find((x) => x.id === id);
|
||||
|
||||
if (currentEdge) {
|
||||
const { source, sourceHandle } = currentEdge;
|
||||
const operatorType = getOperatorTypeFromId(source);
|
||||
// After deleting the edge, set the corresponding field in the node's form field to undefined
|
||||
switch (operatorType) {
|
||||
case Operator.Relevant:
|
||||
updateNodeForm(source, {
|
||||
[sourceHandle as string]: undefined,
|
||||
});
|
||||
break;
|
||||
case Operator.Categorize:
|
||||
if (sourceHandle)
|
||||
updateNodeForm(source, undefined, [
|
||||
'category_description',
|
||||
sourceHandle,
|
||||
'to',
|
||||
]);
|
||||
break;
|
||||
case Operator.Switch: {
|
||||
updateSwitchFormData(source, sourceHandle, undefined);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
set({
|
||||
edges: edges.filter((edge) => edge.id !== id),
|
||||
});
|
||||
},
|
||||
deleteEdgeBySourceAndSourceHandle: ({
|
||||
source,
|
||||
sourceHandle,
|
||||
}: Partial<Connection>) => {
|
||||
const { edges } = get();
|
||||
const nextEdges = edges.filter(
|
||||
(edge) =>
|
||||
edge.source !== source || edge.sourceHandle !== sourceHandle,
|
||||
);
|
||||
set({
|
||||
edges: nextEdges,
|
||||
});
|
||||
},
|
||||
deleteNodeById: (id: string) => {
|
||||
const { nodes, edges } = get();
|
||||
set({
|
||||
nodes: nodes.filter((node) => node.id !== id),
|
||||
edges: edges
|
||||
.filter((edge) => edge.source !== id)
|
||||
.filter((edge) => edge.target !== id),
|
||||
});
|
||||
},
|
||||
deleteIterationNodeById: (id: string) => {
|
||||
const { nodes, edges } = get();
|
||||
const children = nodes.filter((node) => node.parentId === id);
|
||||
set({
|
||||
nodes: nodes.filter((node) => node.id !== id && node.parentId !== id),
|
||||
edges: edges.filter(
|
||||
(edge) =>
|
||||
edge.source !== id &&
|
||||
edge.target !== id &&
|
||||
!children.some(
|
||||
(child) => edge.source === child.id && edge.target === child.id,
|
||||
),
|
||||
),
|
||||
});
|
||||
},
|
||||
findNodeByName: (name: Operator) => {
|
||||
return get().nodes.find((x) => x.data.label === name);
|
||||
},
|
||||
updateNodeForm: (
|
||||
nodeId: string,
|
||||
values: any,
|
||||
path: (string | number)[] = [],
|
||||
) => {
|
||||
const nextNodes = get().nodes.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
let nextForm: Record<string, unknown> = { ...node.data.form };
|
||||
if (path.length === 0) {
|
||||
nextForm = Object.assign(nextForm, values);
|
||||
} else {
|
||||
lodashSet(nextForm, path, values);
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
form: nextForm,
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
set({
|
||||
nodes: nextNodes,
|
||||
});
|
||||
|
||||
return nextNodes;
|
||||
},
|
||||
updateSwitchFormData: (source, sourceHandle, target) => {
|
||||
const { updateNodeForm } = get();
|
||||
if (sourceHandle) {
|
||||
if (sourceHandle === SwitchElseTo) {
|
||||
updateNodeForm(source, target, [SwitchElseTo]);
|
||||
} else {
|
||||
const operatorIndex = getOperatorIndex(sourceHandle);
|
||||
if (operatorIndex) {
|
||||
updateNodeForm(source, target, [
|
||||
'conditions',
|
||||
Number(operatorIndex) - 1, // The index is the conditions form index
|
||||
'to',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateMutableNodeFormItem: (id: string, field: string, value: any) => {
|
||||
const { nodes } = get();
|
||||
const idx = nodes.findIndex((x) => x.id === id);
|
||||
if (idx) {
|
||||
lodashSet(nodes, [idx, 'data', 'form', field], value);
|
||||
}
|
||||
},
|
||||
updateNodeName: (id, name) => {
|
||||
if (id) {
|
||||
set({
|
||||
nodes: get().nodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
node.data.name = name;
|
||||
}
|
||||
|
||||
return node;
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
setClickedNodeId: (id?: string) => {
|
||||
set({ clickedNodeId: id });
|
||||
},
|
||||
generateNodeName: (name: string) => {
|
||||
const { nodes } = get();
|
||||
|
||||
return generateNodeNamesWithIncreasingIndex(name, nodes);
|
||||
},
|
||||
})),
|
||||
{ name: 'graph' },
|
||||
),
|
||||
);
|
||||
|
||||
export default useGraphStore;
|
106
web/src/pages/agent/utils.test.ts
Normal file
106
web/src/pages/agent/utils.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import customer_service from '../../../../graph/test/dsl_examples/customer_service.json';
|
||||
import headhunter_zh from '../../../../graph/test/dsl_examples/headhunter_zh.json';
|
||||
import interpreter from '../../../../graph/test/dsl_examples/interpreter.json';
|
||||
import retrievalRelevantRewriteAndGenerate from '../../../../graph/test/dsl_examples/retrieval_relevant_rewrite_and_generate.json';
|
||||
import { dsl } from './mock';
|
||||
import { buildNodesAndEdgesFromDSLComponents } from './utils';
|
||||
|
||||
test('buildNodesAndEdgesFromDSLComponents', () => {
|
||||
const { edges, nodes } = buildNodesAndEdgesFromDSLComponents(dsl.components);
|
||||
|
||||
expect(nodes.length).toEqual(4);
|
||||
expect(edges.length).toEqual(4);
|
||||
|
||||
expect(edges).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
source: 'begin',
|
||||
target: 'Answer:China',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
source: 'Answer:China',
|
||||
target: 'Retrieval:China',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
source: 'Retrieval:China',
|
||||
target: 'Generate:China',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
source: 'Generate:China',
|
||||
target: 'Answer:China',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('build nodes and edges from headhunter_zh dsl', () => {
|
||||
const { edges, nodes } = buildNodesAndEdgesFromDSLComponents(
|
||||
headhunter_zh.components,
|
||||
);
|
||||
console.info('node length', nodes.length);
|
||||
console.info('edge length', edges.length);
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'headhunter_zh.json'),
|
||||
JSON.stringify({ edges, nodes }, null, 4),
|
||||
);
|
||||
console.log('JSON data is saved.');
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
expect(nodes.length).toEqual(12);
|
||||
});
|
||||
|
||||
test('build nodes and edges from customer_service dsl', () => {
|
||||
const { edges, nodes } = buildNodesAndEdgesFromDSLComponents(
|
||||
customer_service.components,
|
||||
);
|
||||
console.info('node length', nodes.length);
|
||||
console.info('edge length', edges.length);
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'customer_service.json'),
|
||||
JSON.stringify({ edges, nodes }, null, 4),
|
||||
);
|
||||
console.log('JSON data is saved.');
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
expect(nodes.length).toEqual(12);
|
||||
});
|
||||
|
||||
test('build nodes and edges from interpreter dsl', () => {
|
||||
const { edges, nodes } = buildNodesAndEdgesFromDSLComponents(
|
||||
interpreter.components,
|
||||
);
|
||||
console.info('node length', nodes.length);
|
||||
console.info('edge length', edges.length);
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'interpreter.json'),
|
||||
JSON.stringify({ edges, nodes }, null, 4),
|
||||
);
|
||||
console.log('JSON data is saved.');
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
expect(nodes.length).toEqual(12);
|
||||
});
|
||||
|
||||
test('build nodes and edges from chat bot dsl', () => {
|
||||
const { edges, nodes } = buildNodesAndEdgesFromDSLComponents(
|
||||
retrievalRelevantRewriteAndGenerate.components,
|
||||
);
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'retrieval_relevant_rewrite_and_generate.json'),
|
||||
JSON.stringify({ edges, nodes }, null, 4),
|
||||
);
|
||||
console.log('JSON data is saved.');
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
expect(nodes.length).toEqual(12);
|
||||
});
|
418
web/src/pages/agent/utils.ts
Normal file
418
web/src/pages/agent/utils.ts
Normal file
@ -0,0 +1,418 @@
|
||||
import {
|
||||
DSLComponents,
|
||||
ICategorizeItem,
|
||||
ICategorizeItemResult,
|
||||
RAGFlowNodeType,
|
||||
} from '@/interfaces/database/flow';
|
||||
import { removeUselessFieldsFromValues } from '@/utils/form';
|
||||
import { Edge, Node, Position, XYPosition } from '@xyflow/react';
|
||||
import { FormInstance, FormListFieldData } from 'antd';
|
||||
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 { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
CategorizeAnchorPointPositions,
|
||||
NoDebugOperatorsList,
|
||||
NodeMap,
|
||||
Operator,
|
||||
} from './constant';
|
||||
import { IPosition } from './interface';
|
||||
|
||||
const buildEdges = (
|
||||
operatorIds: string[],
|
||||
currentId: string,
|
||||
allEdges: Edge[],
|
||||
isUpstream = false,
|
||||
componentName: string,
|
||||
nodeParams: Record<string, unknown>,
|
||||
) => {
|
||||
operatorIds.forEach((cur) => {
|
||||
const source = isUpstream ? cur : currentId;
|
||||
const target = isUpstream ? currentId : cur;
|
||||
if (!allEdges.some((e) => e.source === source && e.target === target)) {
|
||||
const edge: Edge = {
|
||||
id: uuidv4(),
|
||||
label: '',
|
||||
// type: 'step',
|
||||
source: source,
|
||||
target: target,
|
||||
// markerEnd: {
|
||||
// type: MarkerType.ArrowClosed,
|
||||
// color: 'rgb(157 149 225)',
|
||||
// width: 20,
|
||||
// height: 20,
|
||||
// },
|
||||
};
|
||||
if (componentName === Operator.Categorize && !isUpstream) {
|
||||
const categoryDescription =
|
||||
nodeParams.category_description as ICategorizeItemResult;
|
||||
|
||||
const name = Object.keys(categoryDescription).find(
|
||||
(x) => categoryDescription[x].to === target,
|
||||
);
|
||||
|
||||
if (name) {
|
||||
edge.sourceHandle = name;
|
||||
}
|
||||
}
|
||||
allEdges.push(edge);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const buildNodesAndEdgesFromDSLComponents = (data: DSLComponents) => {
|
||||
const nodes: Node[] = [];
|
||||
let edges: Edge[] = [];
|
||||
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const downstream = [...value.downstream];
|
||||
const upstream = [...value.upstream];
|
||||
const { component_name: componentName, params } = value.obj;
|
||||
nodes.push({
|
||||
id: key,
|
||||
type: NodeMap[value.obj.component_name as Operator] || 'ragNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: componentName,
|
||||
name: humanId(),
|
||||
form: params,
|
||||
},
|
||||
sourcePosition: Position.Left,
|
||||
targetPosition: Position.Right,
|
||||
});
|
||||
|
||||
buildEdges(upstream, key, edges, true, componentName, params);
|
||||
buildEdges(downstream, key, edges, false, componentName, params);
|
||||
});
|
||||
|
||||
return { nodes, edges };
|
||||
};
|
||||
|
||||
const buildComponentDownstreamOrUpstream = (
|
||||
edges: Edge[],
|
||||
nodeId: string,
|
||||
isBuildDownstream = true,
|
||||
) => {
|
||||
return edges
|
||||
.filter((y) => y[isBuildDownstream ? 'source' : 'target'] === nodeId)
|
||||
.map((y) => y[isBuildDownstream ? 'target' : 'source']);
|
||||
};
|
||||
|
||||
const removeUselessDataInTheOperator = curry(
|
||||
(operatorName: string, params: Record<string, unknown>) => {
|
||||
if (
|
||||
operatorName === Operator.Generate ||
|
||||
operatorName === Operator.Categorize
|
||||
) {
|
||||
return removeUselessFieldsFromValues(params, '');
|
||||
}
|
||||
return params;
|
||||
},
|
||||
);
|
||||
// initialize data for operators without parameters
|
||||
// const initializeOperatorParams = curry((operatorName: string, values: any) => {
|
||||
// if (isEmpty(values)) {
|
||||
// return initialFormValuesMap[operatorName as Operator];
|
||||
// }
|
||||
// return values;
|
||||
// });
|
||||
|
||||
const buildOperatorParams = (operatorName: string) =>
|
||||
pipe(
|
||||
removeUselessDataInTheOperator(operatorName),
|
||||
// initializeOperatorParams(operatorName), // Final processing, for guarantee
|
||||
);
|
||||
|
||||
// construct a dsl based on the node information of the graph
|
||||
export const buildDslComponentsByGraph = (
|
||||
nodes: RAGFlowNodeType[],
|
||||
edges: Edge[],
|
||||
oldDslComponents: DSLComponents,
|
||||
): DSLComponents => {
|
||||
const components: DSLComponents = {};
|
||||
|
||||
nodes
|
||||
?.filter((x) => x.data.label !== Operator.Note)
|
||||
.forEach((x) => {
|
||||
const id = x.id;
|
||||
const operatorName = x.data.label;
|
||||
components[id] = {
|
||||
obj: {
|
||||
...(oldDslComponents[id]?.obj ?? {}),
|
||||
component_name: operatorName,
|
||||
params:
|
||||
buildOperatorParams(operatorName)(
|
||||
x.data.form as Record<string, unknown>,
|
||||
) ?? {},
|
||||
},
|
||||
downstream: buildComponentDownstreamOrUpstream(edges, id, true),
|
||||
upstream: buildComponentDownstreamOrUpstream(edges, id, false),
|
||||
parent_id: x?.parentId,
|
||||
};
|
||||
});
|
||||
|
||||
return components;
|
||||
};
|
||||
|
||||
export const receiveMessageError = (res: any) =>
|
||||
res && (res?.response.status !== 200 || res?.data?.code !== 0);
|
||||
|
||||
// Replace the id in the object with text
|
||||
export const replaceIdWithText = (
|
||||
obj: Record<string, unknown> | unknown[] | unknown,
|
||||
getNameById: (id?: string) => string | undefined,
|
||||
) => {
|
||||
if (isObject(obj)) {
|
||||
const ret: Record<string, unknown> | unknown[] = Array.isArray(obj)
|
||||
? []
|
||||
: {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const val = (obj as Record<string, unknown>)[key];
|
||||
const text = typeof val === 'string' ? getNameById(val) : undefined;
|
||||
(ret as Record<string, unknown>)[key] = text
|
||||
? text
|
||||
: replaceIdWithText(val, getNameById);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
export const isEdgeEqual = (previous: Edge, current: Edge) =>
|
||||
previous.source === current.source &&
|
||||
previous.target === current.target &&
|
||||
previous.sourceHandle === current.sourceHandle;
|
||||
|
||||
export const buildNewPositionMap = (
|
||||
currentKeys: string[],
|
||||
previousPositionMap: Record<string, IPosition>,
|
||||
) => {
|
||||
// index in use
|
||||
const indexesInUse = Object.values(previousPositionMap).map((x) => x.idx);
|
||||
const previousKeys = Object.keys(previousPositionMap);
|
||||
const intersectionKeys = intersectionWith(
|
||||
previousKeys,
|
||||
currentKeys,
|
||||
(categoryDataKey: string, positionMapKey: string) =>
|
||||
categoryDataKey === positionMapKey,
|
||||
);
|
||||
// difference set
|
||||
const currentDifferenceKeys = currentKeys.filter(
|
||||
(x) => !intersectionKeys.some((y: string) => y === x),
|
||||
);
|
||||
const newPositionMap = currentDifferenceKeys.reduce<
|
||||
Record<string, IPosition>
|
||||
>((pre, cur) => {
|
||||
// take a coordinate
|
||||
const effectiveIdxes = CategorizeAnchorPointPositions.map(
|
||||
(x, idx) => idx,
|
||||
).filter((x) => !indexesInUse.some((y) => y === x));
|
||||
const idx = sample(effectiveIdxes);
|
||||
if (idx !== undefined) {
|
||||
indexesInUse.push(idx);
|
||||
pre[cur] = { ...CategorizeAnchorPointPositions[idx], idx };
|
||||
}
|
||||
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
return { intersectionKeys, newPositionMap };
|
||||
};
|
||||
|
||||
export const isKeysEqual = (currentKeys: string[], previousKeys: string[]) => {
|
||||
return isEqual(currentKeys.sort(), previousKeys.sort());
|
||||
};
|
||||
|
||||
export const getOperatorIndex = (handleTitle: string) => {
|
||||
return handleTitle.split(' ').at(-1);
|
||||
};
|
||||
|
||||
// Get the value of other forms except itself
|
||||
export const getOtherFieldValues = (
|
||||
form: FormInstance,
|
||||
formListName: string = 'items',
|
||||
field: FormListFieldData,
|
||||
latestField: string,
|
||||
) =>
|
||||
(form.getFieldValue([formListName]) ?? [])
|
||||
.map((x: any) => {
|
||||
return get(x, latestField);
|
||||
})
|
||||
.filter(
|
||||
(x: string) =>
|
||||
x !== form.getFieldValue([formListName, field.name, latestField]),
|
||||
);
|
||||
|
||||
export const generateSwitchHandleText = (idx: number) => {
|
||||
return `Case ${idx + 1}`;
|
||||
};
|
||||
|
||||
export const getNodeDragHandle = (nodeType?: string) => {
|
||||
return nodeType === Operator.Note ? '.note-drag-handle' : undefined;
|
||||
};
|
||||
|
||||
const splitName = (name: string) => {
|
||||
const names = name.split('_');
|
||||
const type = names.at(0);
|
||||
const index = Number(names.at(-1));
|
||||
|
||||
return { type, index };
|
||||
};
|
||||
|
||||
export const generateNodeNamesWithIncreasingIndex = (
|
||||
name: string,
|
||||
nodes: RAGFlowNodeType[],
|
||||
) => {
|
||||
const templateNameList = nodes
|
||||
.filter((x) => {
|
||||
const temporaryName = x.data.name;
|
||||
|
||||
const { type, index } = splitName(temporaryName);
|
||||
|
||||
return (
|
||||
temporaryName.match(/_/g)?.length === 1 &&
|
||||
type === name &&
|
||||
!isNaN(index)
|
||||
);
|
||||
})
|
||||
.map((x) => {
|
||||
const temporaryName = x.data.name;
|
||||
const { index } = splitName(temporaryName);
|
||||
|
||||
return {
|
||||
idx: index,
|
||||
name: temporaryName,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.idx - b.idx);
|
||||
|
||||
let index: number = 0;
|
||||
for (let i = 0; i < templateNameList.length; i++) {
|
||||
const idx = templateNameList[i]?.idx;
|
||||
const nextIdx = templateNameList[i + 1]?.idx;
|
||||
if (idx + 1 !== nextIdx) {
|
||||
index = idx + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return `${name}_${index}`;
|
||||
};
|
||||
|
||||
export const duplicateNodeForm = (nodeData?: RAGFlowNodeType['data']) => {
|
||||
const form: Record<string, any> = { ...(nodeData?.form ?? {}) };
|
||||
|
||||
// Delete the downstream node corresponding to the to field of the Categorize operator
|
||||
if (nodeData?.label === Operator.Categorize) {
|
||||
form.category_description = Object.keys(form.category_description).reduce<
|
||||
Record<string, Record<string, any>>
|
||||
>((pre, cur) => {
|
||||
pre[cur] = {
|
||||
...form.category_description[cur],
|
||||
to: undefined,
|
||||
};
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Delete the downstream nodes corresponding to the yes and no fields of the Relevant operator
|
||||
if (nodeData?.label === Operator.Relevant) {
|
||||
form.yes = undefined;
|
||||
form.no = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...(nodeData ?? { label: '' }),
|
||||
form,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDrawerWidth = () => {
|
||||
return window.innerWidth > 1278 ? '40%' : 470;
|
||||
};
|
||||
|
||||
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: RAGFlowNodeType[],
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* convert the following object into a list
|
||||
*
|
||||
* {
|
||||
"product_related": {
|
||||
"description": "The question is about product usage, appearance and how it works.",
|
||||
"examples": "Why it always beaming?\nHow to install it onto the wall?\nIt leaks, what to do?",
|
||||
"to": "generate:0"
|
||||
}
|
||||
}
|
||||
*/
|
||||
export const buildCategorizeListFromObject = (
|
||||
categorizeItem: ICategorizeItemResult,
|
||||
) => {
|
||||
// Categorize's to field has two data sources, with edges as the data source.
|
||||
// Changes in the edge or to field need to be synchronized to the form field.
|
||||
return Object.keys(categorizeItem)
|
||||
.reduce<Array<ICategorizeItem>>((pre, cur) => {
|
||||
// synchronize edge data to the to field
|
||||
|
||||
pre.push({ name: cur, ...categorizeItem[cur] });
|
||||
return pre;
|
||||
}, [])
|
||||
.sort((a, b) => a.index - b.index);
|
||||
};
|
@ -39,7 +39,11 @@ export function AgentCard({ data }: IProps) {
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Button variant="icon" size="icon" onClick={navigateToAgent}>
|
||||
<Button
|
||||
variant="icon"
|
||||
size="icon"
|
||||
onClick={navigateToAgent(data.id)}
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Button>
|
||||
<Button variant="icon" size="icon">
|
||||
|
@ -215,7 +215,7 @@ const routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.Agent,
|
||||
path: `${Routes.Agent}/:id`,
|
||||
layout: false,
|
||||
component: `@/pages${Routes.Agent}`,
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user