mirror of
https://github.com/labring/FastGPT.git
synced 2026-02-27 01:02:22 +08:00
perf: workflow node past;feat: table export (#6250)
* perf: workflow node past * feat: table export * feat: table export
This commit is contained in:
@@ -219,6 +219,7 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown table {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.markdown table th {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Box } from '@chakra-ui/react';
|
||||
import { CodeClassNameEnum, mdTextFormat } from './utils';
|
||||
import { useCreation } from 'ahooks';
|
||||
import type { AProps } from './A';
|
||||
import MarkdownTable from '@fastgpt/web/components/common/Markdown/MarkdownTable';
|
||||
|
||||
const CodeLight = dynamic(() => import('./codeBlock/CodeLight'), { ssr: false });
|
||||
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
|
||||
@@ -57,6 +58,7 @@ const MarkdownRender = ({
|
||||
img: (props: any) => <Image {...props} alt={props.alt} chatAuthData={chatAuthData} />,
|
||||
pre: RewritePre,
|
||||
code: Code,
|
||||
table: MarkdownTable as any,
|
||||
a: (props: any) => (
|
||||
<A
|
||||
{...props}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { type Node, useKeyPress } from 'reactflow';
|
||||
import { type Node, useKeyPress, useReactFlow } from 'reactflow';
|
||||
import { type FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { useWorkflowUtils } from './useUtils';
|
||||
@@ -18,6 +18,7 @@ export const useKeyboard = () => {
|
||||
const getNodes = useContextSelector(WorkflowBufferDataContext, (v) => v.getNodes);
|
||||
const setNodes = useContextSelector(WorkflowBufferDataContext, (v) => v.setNodes);
|
||||
const mouseInCanvas = useContextSelector(WorkflowUIContext, (v) => v.mouseInCanvas);
|
||||
const mousePosition = useContextSelector(WorkflowUIContext, (v) => v.mousePosition);
|
||||
|
||||
const { getMyModelList } = useSystemStore();
|
||||
const { data: myModels } = useRequest2(getMyModelList, {
|
||||
@@ -26,6 +27,7 @@ export const useKeyboard = () => {
|
||||
|
||||
const { copyData } = useCopyData();
|
||||
const { computedNewNodeName } = useWorkflowUtils();
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
|
||||
const isDowningCtrl = useKeyPress(['Meta', 'Control']);
|
||||
|
||||
@@ -55,43 +57,61 @@ export const useKeyboard = () => {
|
||||
|
||||
const onPaste = useCallback(async () => {
|
||||
if (hasInputtingElement()) return;
|
||||
|
||||
// Only paste if mouse is in canvas and we have mouse position
|
||||
if (!mouseInCanvas || !mousePosition) return;
|
||||
|
||||
const copyResult = await navigator.clipboard.readText();
|
||||
try {
|
||||
const parseData = JSON.parse(copyResult) as Node<FlowNodeItemType, string | undefined>[];
|
||||
// check is array
|
||||
if (!Array.isArray(parseData)) return;
|
||||
// filter workflow data
|
||||
const newNodes = parseData
|
||||
.filter(
|
||||
(item) => !!item.type && item.data?.unique !== true && item.type !== FlowNodeTypeEnum.loop
|
||||
)
|
||||
.map((item) => {
|
||||
const nodeId = getNanoid();
|
||||
item.data.inputs.forEach((input) => {
|
||||
if (input.key === 'model') {
|
||||
if (!myModels?.has(input.value)) input.value = undefined;
|
||||
}
|
||||
});
|
||||
return {
|
||||
// reset id
|
||||
...item,
|
||||
id: nodeId,
|
||||
data: {
|
||||
...item.data,
|
||||
name: computedNewNodeName({
|
||||
templateName: item.data?.name || '',
|
||||
flowNodeType: item.data?.flowNodeType || '',
|
||||
pluginId: item.data?.pluginId
|
||||
}),
|
||||
nodeId,
|
||||
parentNodeId: undefined
|
||||
},
|
||||
position: {
|
||||
x: item.position.x + 100,
|
||||
y: item.position.y + 100
|
||||
}
|
||||
};
|
||||
const filteredData = parseData.filter(
|
||||
(item) => !!item.type && item.data?.unique !== true && item.type !== FlowNodeTypeEnum.loop
|
||||
);
|
||||
|
||||
if (filteredData.length === 0) return;
|
||||
|
||||
// Convert mouse screen position to flow position
|
||||
const pasteFlowPosition = screenToFlowPosition(mousePosition);
|
||||
|
||||
// Calculate the bounding box of the original nodes
|
||||
const minX = Math.min(...filteredData.map((item) => item.position.x));
|
||||
const minY = Math.min(...filteredData.map((item) => item.position.y));
|
||||
const maxX = Math.max(...filteredData.map((item) => item.position.x));
|
||||
const maxY = Math.max(...filteredData.map((item) => item.position.y));
|
||||
const originalCenterX = (minX + maxX) / 2;
|
||||
const originalCenterY = (minY + maxY) / 2;
|
||||
|
||||
const newNodes = filteredData.map((item) => {
|
||||
const nodeId = getNanoid();
|
||||
item.data.inputs.forEach((input) => {
|
||||
if (input.key === 'model') {
|
||||
if (!myModels?.has(input.value)) input.value = undefined;
|
||||
}
|
||||
});
|
||||
return {
|
||||
// reset id
|
||||
...item,
|
||||
id: nodeId,
|
||||
data: {
|
||||
...item.data,
|
||||
name: computedNewNodeName({
|
||||
templateName: item.data?.name || '',
|
||||
flowNodeType: item.data?.flowNodeType || '',
|
||||
pluginId: item.data?.pluginId
|
||||
}),
|
||||
nodeId,
|
||||
parentNodeId: undefined
|
||||
},
|
||||
// Position nodes relative to the mouse position
|
||||
position: {
|
||||
x: pasteFlowPosition.x + (item.position.x - originalCenterX),
|
||||
y: pasteFlowPosition.y + (item.position.y - originalCenterY)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Reset all node to not select and concat new node
|
||||
setNodes((prev) =>
|
||||
@@ -104,7 +124,15 @@ export const useKeyboard = () => {
|
||||
.concat(newNodes)
|
||||
);
|
||||
} catch (error) {}
|
||||
}, [computedNewNodeName, hasInputtingElement, myModels, setNodes]);
|
||||
}, [
|
||||
computedNewNodeName,
|
||||
hasInputtingElement,
|
||||
mouseInCanvas,
|
||||
mousePosition,
|
||||
myModels,
|
||||
screenToFlowPosition,
|
||||
setNodes
|
||||
]);
|
||||
|
||||
useKeyPressEffect(['ctrl.c', 'meta.c'], (e) => {
|
||||
if (!mouseInCanvas) return;
|
||||
|
||||
@@ -21,6 +21,9 @@ type WorkflowUIContextValue = {
|
||||
/** 鼠标是否在 Canvas 中 */
|
||||
mouseInCanvas: boolean;
|
||||
|
||||
/** 鼠标在 Canvas 中的屏幕位置 */
|
||||
mousePosition: { x: number; y: number } | null;
|
||||
|
||||
/** ReactFlow 包装器 callback ref */
|
||||
reactFlowWrapperCallback: (node: HTMLDivElement | null) => void;
|
||||
|
||||
@@ -50,6 +53,7 @@ export const WorkflowUIContext = createContext<WorkflowUIContextValue>({
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
mouseInCanvas: false,
|
||||
mousePosition: null,
|
||||
reactFlowWrapperCallback: function (_node: HTMLDivElement | null): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
@@ -74,6 +78,7 @@ export const WorkflowUIProvider: React.FC<PropsWithChildren> = ({ children }) =>
|
||||
|
||||
// Canvas 交互
|
||||
const [mouseInCanvas, setMouseInCanvas] = useState(false);
|
||||
const [mousePosition, setMousePosition] = useState<{ x: number; y: number } | null>(null);
|
||||
// 使用 ref 来存储 wrapper 引用和 cleanup 函数
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const cleanupRef = useRef<(() => void) | null>(null);
|
||||
@@ -93,16 +98,23 @@ export const WorkflowUIProvider: React.FC<PropsWithChildren> = ({ children }) =>
|
||||
};
|
||||
const handleMouseOutCanvas = () => {
|
||||
setMouseInCanvas(false);
|
||||
setMousePosition(null);
|
||||
};
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setMousePosition({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
|
||||
node.addEventListener('mouseenter', handleMouseInCanvas);
|
||||
node.addEventListener('mouseleave', handleMouseOutCanvas);
|
||||
node.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
// 存储 cleanup 函数到 ref
|
||||
cleanupRef.current = () => {
|
||||
node.removeEventListener('mouseenter', handleMouseInCanvas);
|
||||
node.removeEventListener('mouseleave', handleMouseOutCanvas);
|
||||
node.removeEventListener('mousemove', handleMouseMove);
|
||||
setMouseInCanvas(false);
|
||||
setMousePosition(null);
|
||||
};
|
||||
} else {
|
||||
(reactFlowWrapper as any).current = null;
|
||||
@@ -139,6 +151,7 @@ export const WorkflowUIProvider: React.FC<PropsWithChildren> = ({ children }) =>
|
||||
hoverEdgeId,
|
||||
setHoverEdgeId,
|
||||
mouseInCanvas,
|
||||
mousePosition,
|
||||
reactFlowWrapperCallback,
|
||||
workflowControlMode,
|
||||
setWorkflowControlMode,
|
||||
@@ -151,6 +164,7 @@ export const WorkflowUIProvider: React.FC<PropsWithChildren> = ({ children }) =>
|
||||
hoverNodeId,
|
||||
hoverEdgeId,
|
||||
mouseInCanvas,
|
||||
mousePosition,
|
||||
reactFlowWrapperCallback,
|
||||
workflowControlMode,
|
||||
setWorkflowControlMode,
|
||||
|
||||
Reference in New Issue
Block a user