mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-07-23 17:24:27 +08:00
feat: add FlowHeader and delete edge (#959)
### What problem does this PR solve? feat: add FlowHeader and delete edge #918 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
parent
21aac545d9
commit
495a6434ec
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"elkjs": "^0.9.3",
|
||||||
"eventsource-parser": "^1.1.2",
|
"eventsource-parser": "^1.1.2",
|
||||||
"i18next": "^23.7.16",
|
"i18next": "^23.7.16",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
@ -11110,6 +11111,11 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.767.tgz",
|
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.767.tgz",
|
||||||
"integrity": "sha512-nzzHfmQqBss7CE3apQHkHjXW77+8w3ubGCIoEijKCJebPufREaFETgGXWTkh32t259F3Kcq+R8MZdFdOJROgYw=="
|
"integrity": "sha512-nzzHfmQqBss7CE3apQHkHjXW77+8w3ubGCIoEijKCJebPufREaFETgGXWTkh32t259F3Kcq+R8MZdFdOJROgYw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/elkjs": {
|
||||||
|
"version": "0.9.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/elkjs/-/elkjs-0.9.3.tgz",
|
||||||
|
"integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
|
||||||
|
},
|
||||||
"node_modules/elliptic": {
|
"node_modules/elliptic": {
|
||||||
"version": "6.5.5",
|
"version": "6.5.5",
|
||||||
"resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.5.5.tgz",
|
"resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.5.5.tgz",
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"elkjs": "^0.9.3",
|
||||||
"eventsource-parser": "^1.1.2",
|
"eventsource-parser": "^1.1.2",
|
||||||
"i18next": "^23.7.16",
|
"i18next": "^23.7.16",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
|
@ -24,7 +24,7 @@ export default {
|
|||||||
copied: '複製成功',
|
copied: '複製成功',
|
||||||
comingSoon: '即將推出',
|
comingSoon: '即將推出',
|
||||||
download: '下載',
|
download: '下載',
|
||||||
close: '关闭',
|
close: '關閉',
|
||||||
preview: '預覽',
|
preview: '預覽',
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
|
@ -84,9 +84,6 @@ export const useHandleNodeContextMenu = (sideWidth: number) => {
|
|||||||
// event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
|
// event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
console.info('clientX:', event.clientX);
|
|
||||||
console.info('clientY:', event.clientY);
|
|
||||||
|
|
||||||
setMenu({
|
setMenu({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
top: event.clientY - 72,
|
top: event.clientY - 72,
|
||||||
|
@ -17,9 +17,13 @@ import 'reactflow/dist/style.css';
|
|||||||
import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
|
import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
|
||||||
|
|
||||||
import FlowDrawer from '../flow-drawer';
|
import FlowDrawer from '../flow-drawer';
|
||||||
import { useHandleDrop, useShowDrawer } from '../hooks';
|
import {
|
||||||
import { initialEdges, initialNodes } from '../mock';
|
useHandleDrop,
|
||||||
import { getLayoutedElements } from '../utils';
|
useHandleKeyUp,
|
||||||
|
useHandleSelectionChange,
|
||||||
|
useShowDrawer,
|
||||||
|
} from '../hooks';
|
||||||
|
import { dsl } from '../mock';
|
||||||
import { TextUpdaterNode } from './node';
|
import { TextUpdaterNode } from './node';
|
||||||
|
|
||||||
const nodeTypes = { textUpdater: TextUpdaterNode };
|
const nodeTypes = { textUpdater: TextUpdaterNode };
|
||||||
@ -29,13 +33,11 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function FlowCanvas({ sideWidth }: IProps) {
|
function FlowCanvas({ sideWidth }: IProps) {
|
||||||
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
|
const [nodes, setNodes] = useState<Node[]>(dsl.graph.nodes);
|
||||||
initialNodes,
|
const [edges, setEdges] = useState<Edge[]>(dsl.graph.edges);
|
||||||
initialEdges,
|
|
||||||
'LR',
|
const { selectedEdges, selectedNodes } = useHandleSelectionChange();
|
||||||
);
|
|
||||||
const [nodes, setNodes] = useState<Node[]>(layoutedNodes);
|
|
||||||
const [edges, setEdges] = useState<Edge[]>(layoutedEdges);
|
|
||||||
const { ref, menu, onNodeContextMenu, onPaneClick } =
|
const { ref, menu, onNodeContextMenu, onPaneClick } =
|
||||||
useHandleNodeContextMenu(sideWidth);
|
useHandleNodeContextMenu(sideWidth);
|
||||||
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
|
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
|
||||||
@ -60,6 +62,8 @@ function FlowCanvas({ sideWidth }: IProps) {
|
|||||||
|
|
||||||
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
|
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
|
||||||
|
|
||||||
|
const { handleKeyUp } = useHandleKeyUp(selectedEdges, selectedNodes);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.info('nodes:', nodes);
|
console.info('nodes:', nodes);
|
||||||
console.info('edges:', edges);
|
console.info('edges:', edges);
|
||||||
@ -82,6 +86,7 @@ function FlowCanvas({ sideWidth }: IProps) {
|
|||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onInit={setReactFlowInstance}
|
onInit={setReactFlowInstance}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
<Controls />
|
<Controls />
|
||||||
|
35
web/src/pages/flow/elk-hooks.ts
Normal file
35
web/src/pages/flow/elk-hooks.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useCallback, useLayoutEffect } from 'react';
|
||||||
|
import { getLayoutedElements } from './elk-utils';
|
||||||
|
|
||||||
|
export const elkOptions = {
|
||||||
|
'elk.algorithm': 'layered',
|
||||||
|
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
|
||||||
|
'elk.spacing.nodeNode': '80',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLayoutGraph = (
|
||||||
|
initialNodes,
|
||||||
|
initialEdges,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
) => {
|
||||||
|
const onLayout = useCallback(({ direction, useInitialNodes = false }) => {
|
||||||
|
const opts = { 'elk.direction': direction, ...elkOptions };
|
||||||
|
const ns = initialNodes;
|
||||||
|
const es = initialEdges;
|
||||||
|
|
||||||
|
getLayoutedElements(ns, es, opts).then(
|
||||||
|
({ nodes: layoutedNodes, edges: layoutedEdges }) => {
|
||||||
|
setNodes(layoutedNodes);
|
||||||
|
setEdges(layoutedEdges);
|
||||||
|
|
||||||
|
// window.requestAnimationFrame(() => fitView());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Calculate the initial layout on mount.
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
onLayout({ direction: 'RIGHT', useInitialNodes: true });
|
||||||
|
}, [onLayout]);
|
||||||
|
};
|
42
web/src/pages/flow/elk-utils.ts
Normal file
42
web/src/pages/flow/elk-utils.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||||
|
import { Edge, Node } from 'reactflow';
|
||||||
|
|
||||||
|
const elk = new ELK();
|
||||||
|
|
||||||
|
export const getLayoutedElements = (
|
||||||
|
nodes: Node[],
|
||||||
|
edges: Edge[],
|
||||||
|
options = {},
|
||||||
|
) => {
|
||||||
|
const isHorizontal = options?.['elk.direction'] === 'RIGHT';
|
||||||
|
const graph = {
|
||||||
|
id: 'root',
|
||||||
|
layoutOptions: options,
|
||||||
|
children: nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
// Adjust the target and source handle positions based on the layout
|
||||||
|
// direction.
|
||||||
|
targetPosition: isHorizontal ? 'left' : 'top',
|
||||||
|
sourcePosition: isHorizontal ? 'right' : 'bottom',
|
||||||
|
|
||||||
|
// Hardcode a width and height for elk to use when layouting.
|
||||||
|
width: 150,
|
||||||
|
height: 50,
|
||||||
|
})),
|
||||||
|
edges: edges,
|
||||||
|
};
|
||||||
|
|
||||||
|
return elk
|
||||||
|
.layout(graph)
|
||||||
|
.then((layoutedGraph) => ({
|
||||||
|
nodes: layoutedGraph.children.map((node) => ({
|
||||||
|
...node,
|
||||||
|
// React Flow expects a position property on the node instead of `x`
|
||||||
|
// and `y` fields.
|
||||||
|
position: { x: node.x, y: node.y },
|
||||||
|
})),
|
||||||
|
|
||||||
|
edges: layoutedGraph.edges,
|
||||||
|
}))
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
3
web/src/pages/flow/header/index.less
Normal file
3
web/src/pages/flow/header/index.less
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.flowHeader {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
26
web/src/pages/flow/header/index.tsx
Normal file
26
web/src/pages/flow/header/index.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Button, Flex } from 'antd';
|
||||||
|
|
||||||
|
import { useSaveGraph } from '../hooks';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const FlowHeader = () => {
|
||||||
|
const { saveGraph } = useSaveGraph();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="end"
|
||||||
|
gap={'large'}
|
||||||
|
className={styles.flowHeader}
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<b>Debug</b>
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" onClick={saveGraph}>
|
||||||
|
<b>Save</b>
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlowHeader;
|
@ -1,6 +1,18 @@
|
|||||||
import { useSetModalState } from '@/hooks/commonHooks';
|
import { useSetModalState } from '@/hooks/commonHooks';
|
||||||
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
import React, {
|
||||||
import { Node, Position, ReactFlowInstance } from 'reactflow';
|
Dispatch,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Node,
|
||||||
|
Position,
|
||||||
|
ReactFlowInstance,
|
||||||
|
useOnSelectionChange,
|
||||||
|
useReactFlow,
|
||||||
|
} from 'reactflow';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export const useHandleDrag = () => {
|
export const useHandleDrag = () => {
|
||||||
@ -75,3 +87,52 @@ export const useShowDrawer = () => {
|
|||||||
showDrawer,
|
showDrawer,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useHandleSelectionChange = () => {
|
||||||
|
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
|
||||||
|
const [selectedEdges, setSelectedEdges] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useOnSelectionChange({
|
||||||
|
onChange: ({ nodes, edges }) => {
|
||||||
|
setSelectedNodes(nodes.map((node) => node.id));
|
||||||
|
setSelectedEdges(edges.map((edge) => edge.id));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { selectedEdges, selectedNodes };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteEdge = (selectedEdges: string[]) => {
|
||||||
|
const { setEdges } = useReactFlow();
|
||||||
|
|
||||||
|
const deleteEdge = useCallback(() => {
|
||||||
|
setEdges((edges) =>
|
||||||
|
edges.filter((edge) => selectedEdges.every((x) => x !== edge.id)),
|
||||||
|
);
|
||||||
|
}, [setEdges, selectedEdges]);
|
||||||
|
|
||||||
|
return deleteEdge;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHandleKeyUp = (
|
||||||
|
selectedEdges: string[],
|
||||||
|
selectedNodes: string[],
|
||||||
|
) => {
|
||||||
|
const deleteEdge = useDeleteEdge(selectedEdges);
|
||||||
|
const handleKeyUp: KeyboardEventHandler = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (e.code === 'Delete') {
|
||||||
|
deleteEdge();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[deleteEdge],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { handleKeyUp };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSaveGraph = () => {
|
||||||
|
const saveGraph = useCallback(() => {}, []);
|
||||||
|
|
||||||
|
return { saveGraph };
|
||||||
|
};
|
||||||
|
@ -3,6 +3,7 @@ import { useState } from 'react';
|
|||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
import FlowCanvas from './canvas';
|
import FlowCanvas from './canvas';
|
||||||
import Sider from './flow-sider';
|
import Sider from './flow-sider';
|
||||||
|
import FlowHeader from './header';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ function RagFlow() {
|
|||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
|
<Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<FlowHeader></FlowHeader>
|
||||||
<Content style={{ margin: '0 16px' }}>
|
<Content style={{ margin: '0 16px' }}>
|
||||||
<FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
|
<FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
|
||||||
</Content>
|
</Content>
|
||||||
|
@ -45,6 +45,100 @@ export const initialEdges = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const dsl = {
|
export const dsl = {
|
||||||
|
graph: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'begin',
|
||||||
|
type: 'textUpdater',
|
||||||
|
position: {
|
||||||
|
x: 50,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
label: 'Begin',
|
||||||
|
},
|
||||||
|
sourcePosition: 'left',
|
||||||
|
targetPosition: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Answer:China',
|
||||||
|
type: 'textUpdater',
|
||||||
|
position: {
|
||||||
|
x: 150,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
label: 'Answer',
|
||||||
|
},
|
||||||
|
sourcePosition: 'left',
|
||||||
|
targetPosition: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Retrieval:China',
|
||||||
|
type: 'textUpdater',
|
||||||
|
position: {
|
||||||
|
x: 250,
|
||||||
|
y: 200,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
label: 'Retrieval',
|
||||||
|
},
|
||||||
|
sourcePosition: 'left',
|
||||||
|
targetPosition: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Generate:China',
|
||||||
|
type: 'textUpdater',
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
label: 'Generate',
|
||||||
|
},
|
||||||
|
sourcePosition: 'left',
|
||||||
|
targetPosition: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: '7facb53d-65c9-43b3-ac55-339c445d3891',
|
||||||
|
label: '',
|
||||||
|
source: 'begin',
|
||||||
|
target: 'Answer:China',
|
||||||
|
markerEnd: {
|
||||||
|
type: 'arrow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7ac83631-502d-410f-a6e7-bec6866a5e99',
|
||||||
|
label: '',
|
||||||
|
source: 'Generate:China',
|
||||||
|
target: 'Answer:China',
|
||||||
|
markerEnd: {
|
||||||
|
type: 'arrow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '0aaab297-5779-43ed-9281-2c4d3741566f',
|
||||||
|
label: '',
|
||||||
|
source: 'Answer:China',
|
||||||
|
target: 'Retrieval:China',
|
||||||
|
markerEnd: {
|
||||||
|
type: 'arrow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3477f9f3-0a7d-400e-af96-a11ea7673183',
|
||||||
|
label: '',
|
||||||
|
source: 'Retrieval:China',
|
||||||
|
target: 'Generate:China',
|
||||||
|
markerEnd: {
|
||||||
|
type: 'arrow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
begin: {
|
begin: {
|
||||||
obj: {
|
obj: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DSLComponents } from '@/interfaces/database/flow';
|
import { DSLComponents } from '@/interfaces/database/flow';
|
||||||
import dagre from 'dagre';
|
import dagre from 'dagre';
|
||||||
import { Edge, Node, Position } from 'reactflow';
|
import { Edge, MarkerType, Node, Position } from 'reactflow';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const buildEdges = (
|
const buildEdges = (
|
||||||
@ -16,9 +16,12 @@ const buildEdges = (
|
|||||||
allEdges.push({
|
allEdges.push({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
label: '',
|
label: '',
|
||||||
type: 'step',
|
// type: 'step',
|
||||||
source: source,
|
source: source,
|
||||||
target: target,
|
target: target,
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.Arrow,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user