perf: workflow node past;feat: table export (#6250)

* perf: workflow node past

* feat: table export

* feat: table export
This commit is contained in:
Archer
2026-01-12 21:07:44 +08:00
committed by GitHub
parent 6ec16cf0cf
commit fea1c4ed14
9 changed files with 212 additions and 41 deletions

View File

@@ -219,6 +219,7 @@
margin-bottom: 0;
}
.markdown table {
position: relative;
width: 100%;
}
.markdown table th {

View File

@@ -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}

View File

@@ -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;

View File

@@ -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,