Workflow experience optimization (#2681)

* perf: workflow handle connect

* feat: pan drag mode

* perf: workflow keyboard and touchTable adapt

* perf: teaxtarea no wheel

* remove render error
This commit is contained in:
Archer
2024-09-12 13:59:44 +08:00
parent 22bb4c1e2e
commit fde1618af2
22 changed files with 250 additions and 189 deletions

View File

@@ -13,16 +13,13 @@ weight: 813
-------
## V4.8.11 更新预告
## V4.8.11 更新说明
1.
2. 新增 - 工作流循环执行节点
3. 新增 - 工作流用户表单输入节点
4. 新增 - 插件输出,支持指定某些字段为工具调用结果
5. 新增 - 插件支持配置使用引导、全局变量和文件输入
6. 新增 - 简易模式支持新的版本管理方式
7. 新增 - 聊天记录滚动加载,不再只加载 30 条
8. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环
9. 优化 - 工作流 handler 性能优化。
10. 修复 - 知识库选择权限问题。
11. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常。
2. 新增 - 聊天记录滚动加载,不再只加载 30 条
3. 新增 - 工作流增加触摸板优先模式
4. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环
5. 优化 - 工作流 handler 性能优化
6. 优化 - 工作流快捷键,避免调试测试时也会触发
7. 修复 - 知识库选择权限问题
8. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常

View File

@@ -2,10 +2,8 @@
export const iconPaths = {
book: () => import('./icons/book.svg'),
visible: () => import('./icons/visible.svg'),
change: () => import('./icons/change.svg'),
chatSend: () => import('./icons/chatSend.svg'),
configmap: () => import('./icons/configmap.svg'),
closeSolid: () => import('./icons/closeSolid.svg'),
collectionLight: () => import('./icons/collectionLight.svg'),
collectionSolid: () => import('./icons/collectionSolid.svg'),
@@ -80,6 +78,7 @@ export const iconPaths = {
'common/voiceLight': () => import('./icons/common/voiceLight.svg'),
'common/warn': () => import('./icons/common/warn.svg'),
'common/wechatFill': () => import('./icons/common/wechatFill.svg'),
configmap: () => import('./icons/configmap.svg'),
copy: () => import('./icons/copy.svg'),
'core/app/aiFill': () => import('./icons/core/app/aiFill.svg'),
'core/app/aiLight': () => import('./icons/core/app/aiLight.svg'),
@@ -199,6 +198,7 @@ export const iconPaths = {
import('./icons/core/workflow/inputType/selectLLM.svg'),
'core/workflow/inputType/switch': () => import('./icons/core/workflow/inputType/switch.svg'),
'core/workflow/inputType/textarea': () => import('./icons/core/workflow/inputType/textarea.svg'),
'core/workflow/mouse': () => import('./icons/core/workflow/mouse.svg'),
'core/workflow/publish': () => import('./icons/core/workflow/publish.svg'),
'core/workflow/redo': () => import('./icons/core/workflow/redo.svg'),
'core/workflow/revertVersion': () => import('./icons/core/workflow/revertVersion.svg'),
@@ -249,6 +249,7 @@ export const iconPaths = {
import('./icons/core/workflow/template/variableUpdate.svg'),
'core/workflow/template/workflowStart': () =>
import('./icons/core/workflow/template/workflowStart.svg'),
'core/workflow/touchTable': () => import('./icons/core/workflow/touchTable.svg'),
'core/workflow/undo': () => import('./icons/core/workflow/undo.svg'),
'core/workflow/upload': () => import('./icons/core/workflow/upload.svg'),
'core/workflow/versionHistories': () => import('./icons/core/workflow/versionHistories.svg'),
@@ -332,5 +333,6 @@ export const iconPaths = {
text: () => import('./icons/text.svg'),
union: () => import('./icons/union.svg'),
user: () => import('./icons/user.svg'),
visible: () => import('./icons/visible.svg'),
wx: () => import('./icons/wx.svg')
};

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 16" >
<path d="M7.00001 4.02376C6.50295 4.02376 6.10001 4.4267 6.10001 4.92376V6.63711C6.10001 7.13417 6.50295 7.53711 7.00001 7.53711C7.49706 7.53711 7.90001 7.13417 7.90001 6.63711V4.92376C7.90001 4.4267 7.49706 4.02376 7.00001 4.02376Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.982849 6.14213C0.982849 5.42966 0.982849 5.07342 1.01738 4.77382C1.29671 2.35052 3.20837 0.438866 5.63167 0.159535C5.93127 0.125 6.2875 0.125 6.99998 0.125C7.71246 0.125 8.0687 0.125 8.3683 0.159535C10.7916 0.438866 12.7032 2.35052 12.9826 4.77382C13.0171 5.07342 13.0171 5.42966 13.0171 6.14213V9.85787C13.0171 10.5703 13.0171 10.9266 12.9826 11.2262C12.7032 13.6495 10.7916 15.5611 8.3683 15.8405C8.0687 15.875 7.71246 15.875 6.99998 15.875C6.2875 15.875 5.93127 15.875 5.63167 15.8405C3.20837 15.5611 1.29671 13.6495 1.01738 11.2262C0.982849 10.9266 0.982849 10.5703 0.982849 9.85787V6.14213ZM11.5171 6.14213V9.85787C11.5171 10.6121 11.5146 10.862 11.4924 11.0544C11.2929 12.7853 9.92745 14.1508 8.19653 14.3503C8.00411 14.3725 7.75422 14.375 6.99998 14.375C6.24574 14.375 5.99586 14.3725 5.80343 14.3503C4.07251 14.1508 2.70704 12.7853 2.50752 11.0544C2.48534 10.862 2.48285 10.6121 2.48285 9.85787V6.14213C2.48285 5.3879 2.48534 5.13801 2.50752 4.94558C2.70704 3.21466 4.07251 1.84919 5.80343 1.64967C5.99586 1.62749 6.24574 1.625 6.99998 1.625C7.75422 1.625 8.00411 1.62749 8.19653 1.64967C9.92745 1.84919 11.2929 3.21466 11.4924 4.94558C11.5146 5.13801 11.5171 5.3879 11.5171 6.14213Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" >
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 15.75C16.2426 15.75 17.25 14.7426 17.25 13.5V4.5C17.25 3.25736 16.2426 2.25 15 2.25H3C1.75736 2.25 0.75 3.25736 0.75 4.5V13.5C0.75 14.7426 1.75736 15.75 3 15.75H15ZM3 3.75H15C15.4142 3.75 15.75 4.08579 15.75 4.5V10.4111H2.25V4.5C2.25 4.08579 2.58579 3.75 3 3.75ZM2.25 11.9111V13.5C2.25 13.9142 2.58579 14.25 3 14.25H8.25002V11.9111H2.25ZM9.75002 14.25V11.9111H15.75V13.5C15.75 13.9142 15.4142 14.25 15 14.25H9.75002Z" />
</svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@@ -35,6 +35,7 @@ const MultipleRowSelect = ({
return (
<>
<Box
className="nowheel"
flex={'1 0 auto'}
// width={0}
px={2}

View File

@@ -100,6 +100,7 @@ export default function Editor({
return (
<Box
className="nowheel"
position={'relative'}
width={'full'}
h={`${height}px`}

View File

@@ -90,12 +90,14 @@
"less_than_or_equal_to": "Less Than or Equal To",
"max_dialog_rounds": "Maximum Number of Dialog Rounds",
"max_tokens": "Maximum Tokens",
"mouse_priority": "Mouse first",
"new_context": "New Context",
"not_contains": "Does Not Contain",
"only_the_reference_type_is_supported": "Only reference type is supported",
"optional_value_type": "Optional Value Type",
"optional_value_type_tip": "You can specify one or more data types. When dynamically adding fields, users can only select the configured types.",
"other_questions": "Other Questions",
"pan_priority": "Touchpad first",
"pass_returned_object_as_output_to_next_nodes": "Pass the object returned in the code as output to the next nodes. The variable name needs to correspond to the return key.",
"plugin": {
"Instruction_Tip": "You can configure an instruction to explain the purpose of the plugin. This instruction will be displayed each time the plugin is used. Supports standard Markdown syntax.",
@@ -151,4 +153,4 @@
"Team cloud": "Team Cloud",
"exit_tips": "Your changes have not been saved. 'Exit directly' will not save your edits."
}
}
}

View File

@@ -90,12 +90,14 @@
"less_than_or_equal_to": "小于等于",
"max_dialog_rounds": "最多携带多少轮对话记录",
"max_tokens": "最大 Tokens",
"mouse_priority": "鼠标优先",
"new_context": "新的上下文",
"not_contains": "不包含",
"only_the_reference_type_is_supported": "仅支持引用类型",
"optional_value_type": "可选的数据类型",
"optional_value_type_tip": "可以指定 1 个或多个数据类型,用户在动态添加字段时,仅可选择配置的类型",
"other_questions": "其他问题",
"pan_priority": "触摸板优先",
"pass_returned_object_as_output_to_next_nodes": "将代码中 return 的对象作为输出,传递给后续的节点。变量名需要对应 return 的 key",
"plugin": {
"Instruction_Tip": "可以配置一段说明,以解释该插件的用途。每次使用插件前,会显示该段说明。支持标准 Markdown 语法。",
@@ -151,4 +153,4 @@
"Team cloud": "团队云端",
"exit_tips": "您的更改尚未保存,「直接退出」将不会保存您的编辑记录。"
}
}
}

View File

@@ -68,7 +68,13 @@ const AIModelSelector = ({ list, onchange, disableTip, ...props }: Props) => {
return (
<MyTooltip label={disableTip}>
<MySelect isDisabled={!!disableTip} list={expandList} {...props} onchange={onSelect} />
<MySelect
className="nowheel"
isDisabled={!!disableTip}
list={expandList}
{...props}
onchange={onSelect}
/>
</MyTooltip>
);
};

View File

@@ -33,10 +33,7 @@ const AppCard = ({
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
useContextSelector(AppContext, (v) => v);
const { historiesDefaultData, onSaveWorkflow, isSaving } = useContextSelector(
WorkflowContext,
(v) => v
);
const { historiesDefaultData } = useContextSelector(WorkflowContext, (v) => v);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();

View File

@@ -1,55 +1,58 @@
import React, { useEffect, useMemo } from 'react';
import React, { useMemo } from 'react';
import { Background, ControlButton, MiniMap, Panel, useReactFlow, useViewport } from 'reactflow';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import styles from './index.module.scss';
import { maxZoom, minZoom } from '../index';
import { useKeyPress } from 'ahooks';
const buttonStyle = {
border: 'none',
borderRadius: '6px',
padding: '7px'
};
const FlowController = React.memo(function FlowController() {
const { fitView, zoomIn, zoomOut } = useReactFlow();
const { zoom } = useViewport();
const { undo, redo, canRedo, canUndo } = useContextSelector(WorkflowContext, (v) => v);
const {
undo,
redo,
canRedo,
canUndo,
workflowControlMode,
setWorkflowControlMode,
mouseInCanvas
} = useContextSelector(WorkflowContext, (v) => v);
const { t } = useTranslation();
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (
(event.key === 'z' || event.key === 'Z') &&
(event.ctrlKey || event.metaKey) &&
event.shiftKey
) {
event.preventDefault();
redo();
} else if (event.key === 'z' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
undo();
} else if ((event.key === '=' || event.key === '+') && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
zoomIn();
} else if (event.key === '-' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
zoomOut();
}
};
document.addEventListener('keydown', keyDownHandler);
return () => {
document.removeEventListener('keydown', keyDownHandler);
};
}, [undo, redo, zoomIn, zoomOut]);
const buttonStyle = {
border: 'none',
borderRadius: '6px',
padding: '7px'
};
// Controller shortcut key
useKeyPress(['ctrl.z', 'meta.z'], (e) => {
e.preventDefault();
if (!mouseInCanvas) return;
undo();
});
useKeyPress(['ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
e.preventDefault();
if (!mouseInCanvas) return;
redo();
});
useKeyPress(['ctrl.add', 'meta.add', 'ctrl.equalsign', 'meta.equalsign'], (e) => {
e.preventDefault();
if (!mouseInCanvas) return;
zoomIn();
});
useKeyPress(['ctrl.dash', 'meta.dash'], (e) => {
e.preventDefault();
if (!mouseInCanvas) return;
zoomOut();
});
const Render = useMemo(() => {
return (
@@ -79,6 +82,35 @@ const FlowController = React.memo(function FlowController() {
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
}}
>
{/* Control Mode */}
<MyTooltip
label={
workflowControlMode === 'select'
? t('workflow:pan_priority')
: t('workflow:mouse_priority')
}
>
<ControlButton
onClick={() => {
setWorkflowControlMode(workflowControlMode === 'select' ? 'drag' : 'select');
}}
style={{
...buttonStyle
}}
className={`${styles.customControlButton}`}
>
<MyIcon
name={
workflowControlMode === 'select'
? 'core/workflow/touchTable'
: 'core/workflow/mouse'
}
/>
</ControlButton>
</MyTooltip>
<Box w="1px" h="20px" bg="gray.200" mx={1.5}></Box>
{/* undo */}
<MyTooltip label={isMac ? t('common:common.undo_tip_mac') : t('common:common.undo_tip')}>
<ControlButton
@@ -107,7 +139,7 @@ const FlowController = React.memo(function FlowController() {
{/* zoom out */}
<MyTooltip
label={isMac ? t('common:common.zoomout_tip_mac') : t('common:common.zoomout_tip')}
label={isMac ? t('common:common.zoomin_tip_mac') : t('common:common.zoomin_tip')}
>
<ControlButton
onClick={() => zoomOut()}
@@ -121,7 +153,7 @@ const FlowController = React.memo(function FlowController() {
{/* zoom in */}
<MyTooltip
label={isMac ? t('common:common.zoomin_tip_mac') : t('common:common.zoomin_tip')}
label={isMac ? t('common:common.zoomout_tip_mac') : t('common:common.zoomout_tip')}
>
<ControlButton
onClick={() => zoomIn()}
@@ -149,7 +181,20 @@ const FlowController = React.memo(function FlowController() {
<Background />
</>
);
}, [isMac, t, undo, buttonStyle, canUndo, redo, canRedo, zoom, zoomOut, zoomIn, fitView]);
}, [
workflowControlMode,
isMac,
t,
undo,
canUndo,
redo,
canRedo,
zoom,
setWorkflowControlMode,
zoomOut,
zoomIn,
fitView
]);
return Render;
});

View File

@@ -3,7 +3,8 @@ import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
type FormType = {
versionName: string;
isPublish: boolean | undefined;

View File

@@ -1,20 +1,21 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useTranslation } from 'next-i18next';
import { Node } from 'reactflow';
import { Node, useKeyPress } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import { useWorkflowUtils } from './useUtils';
import { useKeyPress as useKeyPressEffect } from 'ahooks';
export const useKeyboard = () => {
const { t } = useTranslation();
const { setNodes, onSaveWorkflow } = useContextSelector(WorkflowContext, (v) => v);
const { setNodes, mouseInCanvas } = useContextSelector(WorkflowContext, (v) => v);
const { copyData } = useCopyData();
const { computedNewNodeName } = useWorkflowUtils();
const [isDowningCtrl, setIsDowningCtrl] = useState(false);
const isDowningCtrl = useKeyPress(['Meta', 'Control']);
const hasInputtingElement = useCallback(() => {
const activeElement = document.activeElement;
@@ -84,48 +85,20 @@ export const useKeyboard = () => {
} catch (error) {}
}, [computedNewNodeName, hasInputtingElement, setNodes]);
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey) {
setIsDowningCtrl(true);
switch (event.key) {
case 'c':
onCopy();
break;
case 'v':
onParse();
break;
case 's':
event.preventDefault();
onSaveWorkflow();
break;
default:
break;
}
}
},
[onCopy, onParse, onSaveWorkflow]
);
const handleKeyUp = useCallback((event: KeyboardEvent) => {
setIsDowningCtrl(false);
}, []);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
useEffect(() => {
document.addEventListener('keyup', handleKeyUp);
return () => {
document.removeEventListener('keyup', handleKeyUp);
};
}, [handleKeyUp]);
useKeyPressEffect(['ctrl.c', 'meta.c'], (e) => {
e.preventDefault();
if (!mouseInCanvas) return;
onCopy();
});
useKeyPressEffect(['ctrl.v', 'meta.v'], (e) => {
e.preventDefault();
if (!mouseInCanvas) return;
onParse();
});
useKeyPressEffect(['ctrl.s', 'meta.s'], (e) => {
e.preventDefault();
if (!mouseInCanvas) return;
});
return {
isDowningCtrl

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ReactFlow, { NodeProps, ReactFlowProvider } from 'reactflow';
import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow';
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
@@ -59,7 +59,10 @@ const edgeTypes = {
};
const Workflow = () => {
const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v);
const { nodes, edges, reactFlowWrapper, workflowControlMode } = useContextSelector(
WorkflowContext,
(v) => v
);
const {
handleNodesChange,
@@ -124,6 +127,7 @@ const Workflow = () => {
connectionLineStyle={connectionLineStyle}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
connectionRadius={50}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgeChange}
onConnect={customOnConnect}
@@ -131,6 +135,17 @@ const Workflow = () => {
onConnectEnd={onConnectEnd}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
panOnScrollSpeed={2}
{...(workflowControlMode === 'select'
? {
selectionMode: SelectionMode.Full,
selectNodesOnDrag: false,
selectionOnDrag: true,
selectionKeyCode: null,
panOnDrag: false,
panOnScroll: true
}
: {})}
>
<FlowController />
<HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />

View File

@@ -35,7 +35,7 @@ const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
/>
</Box>
<ToolSourceHandle nodeId={nodeId} />
<ToolSourceHandle />
</Box>
</NodeCard>
);

View File

@@ -4,15 +4,16 @@ import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import { Connection, Handle, Position } from 'reactflow';
import { useCallback, useMemo } from 'react';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
const handleSize = '14px';
const handleSize = '16px';
type ToolHandleProps = BoxProps & {
nodeId: string;
show: boolean;
};
export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
@@ -22,44 +23,45 @@ export const ToolTargetHandle = ({ nodeId }: ToolHandleProps) => {
const connected = edges.some((edge) => edge.target === nodeId && edge.targetHandle === handleId);
// if top handle is connected, return null
const hidden =
!connected &&
(connectingEdge?.handleId !== NodeOutputKeyEnum.selectedTools ||
edges.some((edge) => edge.targetHandle === getHandleId(nodeId, 'target', 'top')));
const showHandle =
connected || (show && connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools);
const Render = useMemo(() => {
return hidden ? null : (
<MyTooltip label={t('common:core.workflow.tool.Handle')} shouldWrapChildren={false}>
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent',
border: 'none',
width: handleSize,
height: handleSize
}}
type="target"
id={handleId}
position={Position.Top}
>
<Box
className="flow-handle"
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
transform={'translate(0,-30%) rotate(45deg)'}
pointerEvents={'none'}
visibility={'visible'}
/>
</Handle>
</MyTooltip>
return (
<Handle
style={{
borderRadius: '0',
backgroundColor: 'transparent',
border: 'none',
width: handleSize,
height: handleSize,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
top: '-10px',
...(showHandle ? {} : { visibility: 'hidden' })
}}
type="target"
id={handleId}
position={Position.Top}
isConnectableStart={false}
>
<Box
className="flow-handle"
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
transform={'translate(0,0) rotate(45deg)'}
pointerEvents={'none'}
/>
</Handle>
);
}, [handleId, hidden, t]);
}, [handleId, showHandle]);
return Render;
};
export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
export const ToolSourceHandle = () => {
const { t } = useTranslation();
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
@@ -86,7 +88,11 @@ export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
backgroundColor: 'transparent',
border: 'none',
width: handleSize,
height: handleSize
height: handleSize,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bottom: '-10px'
}}
type="source"
id={NodeOutputKeyEnum.selectedTools}
@@ -97,7 +103,7 @@ export const ToolSourceHandle = ({ nodeId }: ToolHandleProps) => {
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
transform={'translate(0,30%) rotate(45deg)'}
transform={'translate(0,0) rotate(45deg)'}
pointerEvents={'none'}
/>
</Handle>

View File

@@ -218,7 +218,7 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
return (
<Handle
style={
!!styles && showHandle
styles && showHandle
? styles
: {
visibility: 'hidden',
@@ -226,10 +226,10 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
...handleSize
}
}
isConnectableEnd={styles && showHandle}
type="target"
id={handleId}
position={position}
isConnectableStart={false}
></Handle>
);
}, [styles, showHandle, transform, handleId, position]);

View File

@@ -145,7 +145,7 @@ const NodeCard = (props: Props) => {
{/* debug */}
<Box px={4} py={3}>
{/* tool target handle */}
{showToolHandle && <ToolTargetHandle nodeId={nodeId} />}
<ToolTargetHandle show={showToolHandle} nodeId={nodeId} />
{/* avatar and name */}
<Flex alignItems={'center'}>

View File

@@ -54,6 +54,7 @@ const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const Render = useMemo(() => {
return (
<JSONEditor
className="nowheel"
bg={'white'}
borderRadius={'sm'}
placeholder={t(item.placeholder as any)}

View File

@@ -10,6 +10,7 @@ const SelectRender = ({ item, nodeId }: RenderInputProps) => {
const Render = useMemo(() => {
return (
<MySelect
className="nowheel"
width={'100%'}
value={item.value}
list={item.list || []}

View File

@@ -6,7 +6,7 @@ import {
storeNode2FlowNode
} from '@/web/core/workflow/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
@@ -48,7 +48,8 @@ import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider';
import { cloneDeep, isEqual } from 'lodash';
import { cloneDeep } from 'lodash';
import { SetState } from 'ahooks/lib/createUseStorageState';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
@@ -64,6 +65,7 @@ type WorkflowContextType = {
basicNodeTemplates: FlowNodeTemplateType[];
filterAppIds?: string[];
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
mouseInCanvas: boolean;
// nodes
nodes: Node<FlowNodeItemType, string | undefined>[];
@@ -145,8 +147,6 @@ type WorkflowContextType = {
edges: StoreEdgeItemType[];
}
| undefined;
onSaveWorkflow: () => Promise<null | undefined>;
isSaving: boolean;
// debug
workflowDebugData:
@@ -182,6 +182,10 @@ type WorkflowContextType = {
| undefined
>
>;
//
workflowControlMode?: 'drag' | 'select';
setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void;
};
type DebugDataType = {
@@ -192,7 +196,6 @@ type DebugDataType = {
};
export const WorkflowContext = createContext<WorkflowContextType>({
isSaving: false,
setConnectingEdge: function (
value: React.SetStateAction<OnConnectStartParams | undefined>
): void {
@@ -205,6 +208,7 @@ export const WorkflowContext = createContext<WorkflowContextType>({
reactFlowWrapper: null,
nodes: [],
nodeList: [],
mouseInCanvas: false,
setNodes: function (
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
): void {
@@ -295,9 +299,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
| undefined {
throw new Error('Function not implemented.');
},
onSaveWorkflow: function (): Promise<null | undefined> {
throw new Error('Function not implemented.');
},
historiesDefaultData: undefined,
setHistoriesDefaultData: function (value: React.SetStateAction<InitProps | undefined>): void {
throw new Error('Function not implemented.');
@@ -323,7 +324,11 @@ export const WorkflowContext = createContext<WorkflowContextType>({
throw new Error('Function not implemented.');
},
canRedo: false,
canUndo: false
canUndo: false,
workflowControlMode: 'drag',
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
throw new Error('Function not implemented.');
}
});
const WorkflowContextProvider = ({
@@ -337,9 +342,34 @@ const WorkflowContextProvider = ({
const { toast } = useToast();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { appDetail, setAppDetail, updateAppDetail } = useContextSelector(AppContext, (v) => v);
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const appId = appDetail._id;
const [workflowControlMode, setWorkflowControlMode] = useLocalStorageState<'drag' | 'select'>(
'workflow-control-mode',
{
defaultValue: 'select',
listenStorageChange: true
}
);
// Mouse in canvas
const [mouseInCanvas, setMouseInCanvas] = useState(false);
useEffect(() => {
const handleMouseInCanvas = (e: MouseEvent) => {
setMouseInCanvas(true);
};
const handleMouseOutCanvas = (e: MouseEvent) => {
setMouseInCanvas(false);
};
reactFlowWrapper?.current?.addEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.addEventListener('mouseleave', handleMouseOutCanvas);
return () => {
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
};
}, [reactFlowWrapper?.current]);
/* edge */
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [hoverEdgeId, setHoverEdgeId] = useState<string>();
@@ -586,33 +616,6 @@ const WorkflowContextProvider = ({
return storeNodes;
});
/* save workflow */
const { runAsync: onSaveWorkflow, loading: isSaving } = useRequest2(async () => {
const { nodes } = await getWorkflowStore();
// version preview / debug mode, not save
if (appDetail.version !== 'v2' || historiesDefaultData || isSaving || !!workflowDebugData)
return;
const storeWorkflow = uiWorkflow2StoreWorkflow({ nodes, edges });
// check valid
if (storeWorkflow.nodes.length === 0 || storeWorkflow.edges.length === 0) {
return;
}
try {
await updateAppDetail({
...storeWorkflow,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
} catch (error) {}
return null;
});
/* debug */
const [workflowDebugData, setWorkflowDebugData] = useState<DebugDataType>();
const onNextNodeDebug = useCallback(
@@ -944,6 +947,10 @@ const WorkflowContextProvider = ({
appId,
reactFlowWrapper,
basicNodeTemplates,
workflowControlMode,
setWorkflowControlMode,
mouseInCanvas,
// node
nodes,
setNodes,
@@ -984,8 +991,6 @@ const WorkflowContextProvider = ({
initData,
flowData2StoreDataAndCheck,
flowData2StoreData,
onSaveWorkflow,
isSaving,
// debug
workflowDebugData,

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import NextHead from '@/components/common/NextHead';
import { delChatRecordById, getChatHistories, getTeamChatInfo } from '@/web/core/chat/api';
import { delChatRecordById, getTeamChatInfo } from '@/web/core/chat/api';
import { useRouter } from 'next/router';
import { Box, Flex, Drawer, DrawerOverlay, DrawerContent, useTheme } from '@chakra-ui/react';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -11,7 +11,6 @@ import ChatHistorySlider from './components/ChatHistorySlider';
import ChatHeader from './components/ChatHeader';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';