mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
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:
@@ -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 发起对话,首轮携带用户选择时会异常。
|
||||
|
@@ -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')
|
||||
};
|
||||
|
@@ -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 |
@@ -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 |
@@ -35,6 +35,7 @@ const MultipleRowSelect = ({
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
className="nowheel"
|
||||
flex={'1 0 auto'}
|
||||
// width={0}
|
||||
px={2}
|
||||
|
@@ -100,6 +100,7 @@ export default function Editor({
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="nowheel"
|
||||
position={'relative'}
|
||||
width={'full'}
|
||||
h={`${height}px`}
|
||||
|
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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": "您的更改尚未保存,「直接退出」将不会保存您的编辑记录。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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;
|
||||
});
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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} />
|
||||
|
@@ -35,7 +35,7 @@ const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<ToolSourceHandle nodeId={nodeId} />
|
||||
<ToolSourceHandle />
|
||||
</Box>
|
||||
</NodeCard>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
@@ -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]);
|
||||
|
@@ -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'}>
|
||||
|
@@ -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)}
|
||||
|
@@ -10,6 +10,7 @@ const SelectRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<MySelect
|
||||
className="nowheel"
|
||||
width={'100%'}
|
||||
value={item.value}
|
||||
list={item.list || []}
|
||||
|
@@ -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,
|
||||
|
@@ -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';
|
||||
|
Reference in New Issue
Block a user