mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-06-04 11:24:00 +08:00
### What problem does this PR solve? feat: fixed issue with threshold translation #882 feat: add NodeContextMenu ### Type of change - [ ] New Feature (non-breaking change which adds functionality)
This commit is contained in:
parent
1e5c5abe58
commit
4cda40c3ef
@ -247,8 +247,8 @@ export default {
|
|||||||
以上就是你需要總結的內容。`,
|
以上就是你需要總結的內容。`,
|
||||||
maxToken: '最大token數',
|
maxToken: '最大token數',
|
||||||
maxTokenMessage: '最大token數是必填項',
|
maxTokenMessage: '最大token數是必填項',
|
||||||
threshold: '臨界點',
|
threshold: '閾值',
|
||||||
thresholdMessage: '臨界點是必填項',
|
thresholdMessage: '閾值是必填項',
|
||||||
maxCluster: '最大聚類數',
|
maxCluster: '最大聚類數',
|
||||||
maxClusterMessage: '最大聚類數是必填項',
|
maxClusterMessage: '最大聚類數是必填項',
|
||||||
randomSeed: '隨機種子',
|
randomSeed: '隨機種子',
|
||||||
|
@ -264,8 +264,8 @@ export default {
|
|||||||
以上就是你需要总结的内容。`,
|
以上就是你需要总结的内容。`,
|
||||||
maxToken: '最大token数',
|
maxToken: '最大token数',
|
||||||
maxTokenMessage: '最大token数是必填项',
|
maxTokenMessage: '最大token数是必填项',
|
||||||
threshold: '临界点',
|
threshold: '阈值',
|
||||||
thresholdMessage: '临界点是必填项',
|
thresholdMessage: '阈值是必填项',
|
||||||
maxCluster: '最大聚类数',
|
maxCluster: '最大聚类数',
|
||||||
maxClusterMessage: '最大聚类数是必填项',
|
maxClusterMessage: '最大聚类数是必填项',
|
||||||
randomSeed: '随机种子',
|
randomSeed: '随机种子',
|
||||||
|
18
web/src/pages/flow/canvas/context-menu/index.less
Normal file
18
web/src/pages/flow/canvas/context-menu/index.less
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.contextMenu {
|
||||||
|
background: white;
|
||||||
|
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: white;
|
||||||
|
}
|
||||||
|
}
|
108
web/src/pages/flow/canvas/context-menu/index.tsx
Normal file
108
web/src/pages/flow/canvas/context-menu/index.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
import { NodeMouseHandler, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
// });
|
||||||
|
|
||||||
|
console.info('clientX:', event.clientX);
|
||||||
|
console.info('clientY:', event.clientY);
|
||||||
|
|
||||||
|
setMenu({
|
||||||
|
id: node.id,
|
||||||
|
top: event.clientY - 72,
|
||||||
|
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 };
|
||||||
|
};
|
@ -4,6 +4,7 @@ import ReactFlow, {
|
|||||||
Controls,
|
Controls,
|
||||||
Edge,
|
Edge,
|
||||||
Node,
|
Node,
|
||||||
|
NodeMouseHandler,
|
||||||
OnConnect,
|
OnConnect,
|
||||||
OnEdgesChange,
|
OnEdgesChange,
|
||||||
OnNodesChange,
|
OnNodesChange,
|
||||||
@ -13,7 +14,10 @@ import ReactFlow, {
|
|||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
|
|
||||||
import { useHandleDrop } from '../hooks';
|
import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
|
||||||
|
|
||||||
|
import FlowDrawer from '../flow-drawer';
|
||||||
|
import { useHandleDrop, useShowDrawer } from '../hooks';
|
||||||
import { TextUpdaterNode } from './node';
|
import { TextUpdaterNode } from './node';
|
||||||
|
|
||||||
const nodeTypes = { textUpdater: TextUpdaterNode };
|
const nodeTypes = { textUpdater: TextUpdaterNode };
|
||||||
@ -42,9 +46,17 @@ const initialEdges = [
|
|||||||
{ id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
|
{ id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function FlowCanvas() {
|
interface IProps {
|
||||||
|
sideWidth: number;
|
||||||
|
showDrawer(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlowCanvas({ sideWidth }: IProps) {
|
||||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||||
|
const { ref, menu, onNodeContextMenu, onPaneClick } =
|
||||||
|
useHandleNodeContextMenu(sideWidth);
|
||||||
|
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
|
||||||
|
|
||||||
const onNodesChange: OnNodesChange = useCallback(
|
const onNodesChange: OnNodesChange = useCallback(
|
||||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||||
@ -60,7 +72,11 @@ function FlowCanvas() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handleDrop, allowDrop } = useHandleDrop(setNodes);
|
const onNodeClick: NodeMouseHandler = useCallback(() => {
|
||||||
|
showDrawer();
|
||||||
|
}, [showDrawer]);
|
||||||
|
|
||||||
|
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.info('nodes:', nodes);
|
console.info('nodes:', nodes);
|
||||||
@ -68,23 +84,30 @@ function FlowCanvas() {
|
|||||||
}, [nodes, edges]);
|
}, [nodes, edges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ height: '100%', width: '100%' }}>
|
||||||
style={{ height: '100%', width: '100%' }}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={allowDrop}
|
|
||||||
>
|
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
ref={ref}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
|
onNodeContextMenu={onNodeContextMenu}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
// fitView
|
fitView
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
|
onPaneClick={onPaneClick}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
onInit={setReactFlowInstance}
|
||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
<Controls />
|
<Controls />
|
||||||
|
{Object.keys(menu).length > 0 && (
|
||||||
|
<NodeContextMenu onClick={onPaneClick} {...(menu as any)} />
|
||||||
|
)}
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
<FlowDrawer visible={drawerVisible} hideModal={hideDrawer}></FlowDrawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
20
web/src/pages/flow/flow-drawer/index.tsx
Normal file
20
web/src/pages/flow/flow-drawer/index.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { Drawer } from 'antd';
|
||||||
|
|
||||||
|
const FlowDrawer = ({ visible, hideModal }: IModalProps<any>) => {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title="Basic Drawer"
|
||||||
|
placement="right"
|
||||||
|
// closable={false}
|
||||||
|
onClose={hideModal}
|
||||||
|
open={visible}
|
||||||
|
getContainer={false}
|
||||||
|
mask={false}
|
||||||
|
>
|
||||||
|
<p>Some contents...</p>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlowDrawer;
|
@ -1,6 +1,5 @@
|
|||||||
import { Avatar, Card, Flex, Layout, Space } from 'antd';
|
import { Avatar, Card, Flex, Layout, Space } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useState } from 'react';
|
|
||||||
import { componentList } from '../mock';
|
import { componentList } from '../mock';
|
||||||
|
|
||||||
import { useHandleDrag } from '../hooks';
|
import { useHandleDrag } from '../hooks';
|
||||||
@ -8,9 +7,13 @@ import styles from './index.less';
|
|||||||
|
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
const FlowSider = () => {
|
interface IProps {
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
setCollapsed: (width: boolean) => void;
|
||||||
const { handleDrag } = useHandleDrag();
|
collapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlowSide = ({ setCollapsed, collapsed }: IProps) => {
|
||||||
|
const { handleDragStart } = useHandleDrag();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sider
|
<Sider
|
||||||
@ -27,7 +30,7 @@ const FlowSider = () => {
|
|||||||
hoverable
|
hoverable
|
||||||
draggable
|
draggable
|
||||||
className={classNames(styles.operatorCard)}
|
className={classNames(styles.operatorCard)}
|
||||||
onDragStart={handleDrag(x.name)}
|
onDragStart={handleDragStart(x.name)}
|
||||||
>
|
>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Space size={15}>
|
<Space size={15}>
|
||||||
@ -45,4 +48,4 @@ const FlowSider = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FlowSider;
|
export default FlowSide;
|
||||||
|
@ -1,47 +1,75 @@
|
|||||||
import React, { Dispatch, SetStateAction, useCallback } from 'react';
|
import { useSetModalState } from '@/hooks/commonHooks';
|
||||||
import { Node } from 'reactflow';
|
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
||||||
|
import { Node, ReactFlowInstance } from 'reactflow';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export const useHandleDrag = () => {
|
export const useHandleDrag = () => {
|
||||||
const handleDrag = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => {
|
(operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => {
|
||||||
console.info(ev.clientX, ev.pageY);
|
ev.dataTransfer.setData('application/reactflow', operatorId);
|
||||||
ev.dataTransfer.setData('operatorId', operatorId);
|
ev.dataTransfer.effectAllowed = 'move';
|
||||||
ev.dataTransfer.setData('startClientX', ev.clientX.toString());
|
|
||||||
ev.dataTransfer.setData('startClientY', ev.clientY.toString());
|
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleDrag };
|
return { handleDragStart };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
|
export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
|
||||||
const allowDrop = (ev: React.DragEvent<HTMLDivElement>) => {
|
const [reactFlowInstance, setReactFlowInstance] =
|
||||||
ev.preventDefault();
|
useState<ReactFlowInstance<any, any>>();
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
||||||
(ev: React.DragEvent<HTMLDivElement>) => {
|
event.preventDefault();
|
||||||
ev.preventDefault();
|
event.dataTransfer.dropEffect = 'move';
|
||||||
const operatorId = ev.dataTransfer.getData('operatorId');
|
}, []);
|
||||||
const startClientX = ev.dataTransfer.getData('startClientX');
|
|
||||||
const startClientY = ev.dataTransfer.getData('startClientY');
|
const onDrop = useCallback(
|
||||||
console.info(operatorId);
|
(event: React.DragEvent<HTMLDivElement>) => {
|
||||||
console.info(ev.pageX, ev.pageY);
|
event.preventDefault();
|
||||||
console.info(ev.clientX, ev.clientY);
|
|
||||||
console.info(ev.movementX, ev.movementY);
|
const type = event.dataTransfer.getData('application/reactflow');
|
||||||
const x = ev.clientX - 200;
|
|
||||||
const y = ev.clientY - 72;
|
// check if the dropped element is valid
|
||||||
setNodes((pre) => {
|
if (typeof type === 'undefined' || !type) {
|
||||||
return pre.concat({
|
return;
|
||||||
id: operatorId,
|
}
|
||||||
position: { x, y },
|
|
||||||
data: { label: operatorId },
|
// reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
|
||||||
});
|
// and you don't need to subtract the reactFlowBounds.left/top anymore
|
||||||
|
// details: https://reactflow.dev/whats-new/2023-11-10
|
||||||
|
const position = reactFlowInstance?.screenToFlowPosition({
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
});
|
});
|
||||||
|
const newNode = {
|
||||||
|
id: uuidv4(),
|
||||||
|
type,
|
||||||
|
position: position || {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
data: { label: `${type} node` },
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nds) => nds.concat(newNode));
|
||||||
},
|
},
|
||||||
[setNodes],
|
[reactFlowInstance, setNodes],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { handleDrop, allowDrop };
|
return { onDrop, onDragOver, setReactFlowInstance };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useShowDrawer = () => {
|
||||||
|
const {
|
||||||
|
visible: drawerVisible,
|
||||||
|
hideModal: hideDrawer,
|
||||||
|
showModal: showDrawer,
|
||||||
|
} = useSetModalState();
|
||||||
|
|
||||||
|
return {
|
||||||
|
drawerVisible,
|
||||||
|
hideDrawer,
|
||||||
|
showDrawer,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
import { Layout } from 'antd';
|
import { Layout } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
import FlowCanvas from './canvas';
|
import FlowCanvas from './canvas';
|
||||||
import Sider from './flow-sider';
|
import Sider from './flow-sider';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
|
||||||
function RagFlow() {
|
function RagFlow() {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<Layout>
|
||||||
<Sider></Sider>
|
<ReactFlowProvider>
|
||||||
<Layout>
|
<Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
|
||||||
<Content style={{ margin: '0 16px' }}>
|
<Layout>
|
||||||
<FlowCanvas></FlowCanvas>
|
<Content style={{ margin: '0 16px' }}>
|
||||||
</Content>
|
<FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
|
||||||
</Layout>
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</ReactFlowProvider>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user