mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 11:43:56 +00:00
feat: The workflow quickly adds applications (#4882)
* feat: add node by handle (#4860) * feat: add node by handle * fix * fix edge filter * fix * move utils * move context * scale handle * move postion to handle params & optimize handle scale (#4878) * move position to handle params * close button scale * perf: node template ui * remove handle scale (#4880) * feat: handle connect * add mouse down duration check (#4881) * perf: long press time * tool handle size * optimize add node by handle (#4883) --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -11,8 +11,9 @@ weight: 790
|
|||||||
## 🚀 新增内容
|
## 🚀 新增内容
|
||||||
|
|
||||||
1. 支持 PG 设置`systemEnv.hnswMaxScanTuples`参数,提高迭代搜索的数据总量。
|
1. 支持 PG 设置`systemEnv.hnswMaxScanTuples`参数,提高迭代搜索的数据总量。
|
||||||
2. 开放飞书和语雀知识库到开源版。
|
2. 工作流调整为单向接入和接出,支持快速的添加下一步节点。
|
||||||
3. gemini 和 claude 最新模型预设。
|
3. 开放飞书和语雀知识库到开源版。
|
||||||
|
4. gemini 和 claude 最新模型预设。
|
||||||
|
|
||||||
## ⚙️ 优化
|
## ⚙️ 优化
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { Flex, Box, type BoxProps } from '@chakra-ui/react';
|
import { Flex, Box, type BoxProps, HStack } from '@chakra-ui/react';
|
||||||
import MyIcon from '../Icon';
|
import MyIcon from '../Icon';
|
||||||
|
|
||||||
type Props<T = string> = Omit<BoxProps, 'onChange'> & {
|
type Props<T = string> = Omit<BoxProps, 'onChange'> & {
|
||||||
@@ -10,9 +10,22 @@ type Props<T = string> = Omit<BoxProps, 'onChange'> & {
|
|||||||
}[];
|
}[];
|
||||||
value: T;
|
value: T;
|
||||||
onChange: (e: T) => void;
|
onChange: (e: T) => void;
|
||||||
|
iconSize?: string;
|
||||||
|
labelSize?: string;
|
||||||
|
iconGap?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FillRowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => {
|
const FillRowTabs = ({
|
||||||
|
list,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
py = '2.5',
|
||||||
|
px = '4',
|
||||||
|
iconSize = '18px',
|
||||||
|
labelSize = 'sm',
|
||||||
|
iconGap = 2,
|
||||||
|
...props
|
||||||
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display={'inline-flex'}
|
display={'inline-flex'}
|
||||||
@@ -28,7 +41,7 @@ const FillRowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{list.map((item) => (
|
{list.map((item) => (
|
||||||
<Flex
|
<HStack
|
||||||
key={item.value}
|
key={item.value}
|
||||||
flex={'1 0 0'}
|
flex={'1 0 0'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
@@ -39,6 +52,7 @@ const FillRowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props
|
|||||||
py={py}
|
py={py}
|
||||||
userSelect={'none'}
|
userSelect={'none'}
|
||||||
whiteSpace={'noWrap'}
|
whiteSpace={'noWrap'}
|
||||||
|
gap={iconGap}
|
||||||
{...(value === item.value
|
{...(value === item.value
|
||||||
? {
|
? {
|
||||||
bg: 'white',
|
bg: 'white',
|
||||||
@@ -53,9 +67,9 @@ const FillRowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props
|
|||||||
onClick: () => onChange(item.value)
|
onClick: () => onChange(item.value)
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{item.icon && <MyIcon name={item.icon as any} mr={1.5} w={'18px'} />}
|
{item.icon && <MyIcon name={item.icon as any} w={iconSize} />}
|
||||||
<Box>{item.label}</Box>
|
<Box fontSize={labelSize}>{item.label}</Box>
|
||||||
</Flex>
|
</HStack>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Array_element": "Array element",
|
"Array_element": "Array element",
|
||||||
"Array_element_index": "Index",
|
"Array_element_index": "Index",
|
||||||
|
"Click": "Click ",
|
||||||
"Code": "Code",
|
"Code": "Code",
|
||||||
"Confirm_sync_node": "It will be updated to the latest node configuration and fields that do not exist in the template will be deleted (including all custom fields).\n\nIf the fields are complex, it is recommended that you copy a node first and then update the original node to facilitate parameter copying.",
|
"Confirm_sync_node": "It will be updated to the latest node configuration and fields that do not exist in the template will be deleted (including all custom fields).\n\nIf the fields are complex, it is recommended that you copy a node first and then update the original node to facilitate parameter copying.",
|
||||||
|
"Drag": "Drag ",
|
||||||
"Node.Open_Node_Course": "Open node course",
|
"Node.Open_Node_Course": "Open node course",
|
||||||
"Node_variables": "Node variables",
|
"Node_variables": "Node variables",
|
||||||
"Quote_prompt_setting": "Quote prompt",
|
"Quote_prompt_setting": "Quote prompt",
|
||||||
@@ -176,6 +178,8 @@
|
|||||||
"text_to_extract": "Text to Extract",
|
"text_to_extract": "Text to Extract",
|
||||||
"these_variables_will_be_input_parameters_for_code_execution": "These variables will be input parameters for code execution",
|
"these_variables_will_be_input_parameters_for_code_execution": "These variables will be input parameters for code execution",
|
||||||
"tool.tool_result": "Tool operation results",
|
"tool.tool_result": "Tool operation results",
|
||||||
|
"to_add_node": "to add",
|
||||||
|
"to_connect_node": "to connect",
|
||||||
"tool_call_termination": "Stop ToolCall",
|
"tool_call_termination": "Stop ToolCall",
|
||||||
"tool_custom_field": "Custom Tool",
|
"tool_custom_field": "Custom Tool",
|
||||||
"tool_field": " Tool Field Parameter Configuration",
|
"tool_field": " Tool Field Parameter Configuration",
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Array_element": "数组元素",
|
"Array_element": "数组元素",
|
||||||
"Array_element_index": "下标",
|
"Array_element_index": "下标",
|
||||||
|
"Click": "点击",
|
||||||
"Code": "代码",
|
"Code": "代码",
|
||||||
"Confirm_sync_node": "将会更新至最新的节点配置,不存在模板中的字段将会被删除(包括所有自定义字段)。\n如果字段较为复杂,建议您先复制一份节点,再更新原来的节点,便于参数复制。",
|
"Confirm_sync_node": "将会更新至最新的节点配置,不存在模板中的字段将会被删除(包括所有自定义字段)。\n如果字段较为复杂,建议您先复制一份节点,再更新原来的节点,便于参数复制。",
|
||||||
|
"Drag": "拖拽",
|
||||||
"Node.Open_Node_Course": "查看节点教程",
|
"Node.Open_Node_Course": "查看节点教程",
|
||||||
"Node_variables": "节点变量",
|
"Node_variables": "节点变量",
|
||||||
"Quote_prompt_setting": "引用提示词配置",
|
"Quote_prompt_setting": "引用提示词配置",
|
||||||
@@ -175,6 +177,8 @@
|
|||||||
"text_content_extraction": "文本内容提取",
|
"text_content_extraction": "文本内容提取",
|
||||||
"text_to_extract": "需要提取的文本",
|
"text_to_extract": "需要提取的文本",
|
||||||
"these_variables_will_be_input_parameters_for_code_execution": "这些变量会作为代码的运行的输入参数",
|
"these_variables_will_be_input_parameters_for_code_execution": "这些变量会作为代码的运行的输入参数",
|
||||||
|
"to_add_node": "添加节点",
|
||||||
|
"to_connect_node": "连接节点",
|
||||||
"tool.tool_result": "工具运行结果",
|
"tool.tool_result": "工具运行结果",
|
||||||
"tool_call_termination": "工具调用终止",
|
"tool_call_termination": "工具调用终止",
|
||||||
"tool_custom_field": "自定义工具变量",
|
"tool_custom_field": "自定义工具变量",
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"Array_element": "陣列元素",
|
"Array_element": "陣列元素",
|
||||||
"Array_element_index": "索引",
|
"Array_element_index": "索引",
|
||||||
|
"Click": "點擊",
|
||||||
"Code": "程式碼",
|
"Code": "程式碼",
|
||||||
"Confirm_sync_node": "將會更新至最新的節點設定,不存在於範本中的欄位將會被刪除(包含所有自訂欄位)。\n如果欄位比較複雜,建議您先複製一份節點,再更新原來的節點,以便複製參數。",
|
"Confirm_sync_node": "將會更新至最新的節點設定,不存在於範本中的欄位將會被刪除(包含所有自訂欄位)。\n如果欄位比較複雜,建議您先複製一份節點,再更新原來的節點,以便複製參數。",
|
||||||
|
"Drag": "拖拽",
|
||||||
"Node.Open_Node_Course": "開啟節點教學課程",
|
"Node.Open_Node_Course": "開啟節點教學課程",
|
||||||
"Node_variables": "節點變數",
|
"Node_variables": "節點變數",
|
||||||
"Quote_prompt_setting": "引用提示詞設定",
|
"Quote_prompt_setting": "引用提示詞設定",
|
||||||
@@ -176,6 +178,8 @@
|
|||||||
"text_to_extract": "要擷取的文字",
|
"text_to_extract": "要擷取的文字",
|
||||||
"these_variables_will_be_input_parameters_for_code_execution": "這些變數會作為程式碼執行的輸入參數",
|
"these_variables_will_be_input_parameters_for_code_execution": "這些變數會作為程式碼執行的輸入參數",
|
||||||
"tool.tool_result": "工具運行結果",
|
"tool.tool_result": "工具運行結果",
|
||||||
|
"to_add_node": "添加節點",
|
||||||
|
"to_connect_node": "連接節點",
|
||||||
"tool_call_termination": "工具呼叫終止",
|
"tool_call_termination": "工具呼叫終止",
|
||||||
"tool_custom_field": "自訂工具變數",
|
"tool_custom_field": "自訂工具變數",
|
||||||
"tool_field": "工具參數設定",
|
"tool_field": "工具參數設定",
|
||||||
|
@@ -32,7 +32,7 @@ import { isProduction } from '@fastgpt/global/common/system/constants';
|
|||||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||||
import {
|
import {
|
||||||
checkWorkflowNodeAndConnection,
|
checkWorkflowNodeAndConnection,
|
||||||
storeEdgesRenderEdge,
|
storeEdge2RenderEdge,
|
||||||
storeNode2FlowNode
|
storeNode2FlowNode
|
||||||
} from '@/web/core/workflow/utils';
|
} from '@/web/core/workflow/utils';
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ const Header = ({
|
|||||||
const { nodes: storeNodes, edges: storeEdges } = form2AppWorkflow(appForm, t);
|
const { nodes: storeNodes, edges: storeEdges } = form2AppWorkflow(appForm, t);
|
||||||
|
|
||||||
const nodes = storeNodes.map((item) => storeNode2FlowNode({ item, t }));
|
const nodes = storeNodes.map((item) => storeNode2FlowNode({ item, t }));
|
||||||
const edges = storeEdges.map((item) => storeEdgesRenderEdge({ edge: item }));
|
const edges = storeEdges.map((item) => storeEdge2RenderEdge({ edge: item }));
|
||||||
|
|
||||||
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||||
|
|
||||||
|
@@ -1,212 +1,46 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Box } from '@chakra-ui/react';
|
||||||
Accordion,
|
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||||
AccordionButton,
|
import { type Node } from 'reactflow';
|
||||||
AccordionIcon,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionPanel,
|
|
||||||
Box,
|
|
||||||
Flex,
|
|
||||||
Grid,
|
|
||||||
HStack,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputLeftElement,
|
|
||||||
css
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import type {
|
|
||||||
NodeTemplateListItemType,
|
|
||||||
NodeTemplateListType
|
|
||||||
} from '@fastgpt/global/core/workflow/type/node.d';
|
|
||||||
import { useReactFlow, type XYPosition } from 'reactflow';
|
|
||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
|
||||||
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
|
||||||
import {
|
|
||||||
AppNodeFlowNodeTypeMap,
|
|
||||||
FlowNodeTypeEnum
|
|
||||||
} from '@fastgpt/global/core/workflow/node/constant';
|
|
||||||
import {
|
|
||||||
getPreviewPluginNode,
|
|
||||||
getSystemPlugTemplates,
|
|
||||||
getPluginGroups,
|
|
||||||
getSystemPluginPaths
|
|
||||||
} from '@/web/core/app/api/plugin';
|
|
||||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
|
||||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
|
||||||
import { workflowNodeTemplateList } from '@fastgpt/web/core/workflow/constants';
|
|
||||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
|
||||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../context';
|
|
||||||
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
|
|
||||||
import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
|
||||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||||
import FolderPath from '@/components/common/folder/Path';
|
|
||||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
|
||||||
import { useWorkflowUtils } from './hooks/useUtils';
|
|
||||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
|
||||||
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
|
||||||
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
|
||||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
|
||||||
import { WorkflowNodeEdgeContext } from '../context/workflowInitContext';
|
import { WorkflowNodeEdgeContext } from '../context/workflowInitContext';
|
||||||
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
|
|
||||||
import MyAvatar from '@fastgpt/web/components/common/Avatar';
|
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import NodeTemplateListHeader from './components/NodeTemplates/header';
|
||||||
|
import NodeTemplateList from './components/NodeTemplates/list';
|
||||||
|
import { useNodeTemplates } from './components/NodeTemplates/useNodeTemplates';
|
||||||
|
|
||||||
type ModuleTemplateListProps = {
|
type ModuleTemplateListProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
type RenderHeaderProps = {
|
|
||||||
templateType: TemplateTypeEnum;
|
|
||||||
onClose: () => void;
|
|
||||||
parentId: ParentIdType;
|
|
||||||
searchKey: string;
|
|
||||||
loadNodeTemplates: (params: any) => void;
|
|
||||||
setSearchKey: (searchKey: string) => void;
|
|
||||||
onUpdateParentId: (parentId: ParentIdType) => void;
|
|
||||||
};
|
|
||||||
type RenderListProps = {
|
|
||||||
templateType: TemplateTypeEnum;
|
|
||||||
templates: NodeTemplateListItemType[];
|
|
||||||
type: TemplateTypeEnum;
|
|
||||||
onClose: () => void;
|
|
||||||
parentId: ParentIdType;
|
|
||||||
setParentId: (parenId: ParentIdType) => any;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum TemplateTypeEnum {
|
export const sliderWidth = 460;
|
||||||
'basic' = 'basic',
|
|
||||||
'systemPlugin' = 'systemPlugin',
|
|
||||||
'teamPlugin' = 'teamPlugin'
|
|
||||||
}
|
|
||||||
|
|
||||||
const sliderWidth = 460;
|
|
||||||
|
|
||||||
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
||||||
const [searchKey, setSearchKey] = useState('');
|
|
||||||
const { feConfigs } = useSystemStore();
|
|
||||||
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
|
|
||||||
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
|
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
|
||||||
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
|
|
||||||
|
|
||||||
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
|
|
||||||
|
|
||||||
const { data: basicNodes } = useRequest2(
|
|
||||||
async () => {
|
|
||||||
if (templateType === TemplateTypeEnum.basic) {
|
|
||||||
return basicNodeTemplates
|
|
||||||
.filter((item) => {
|
|
||||||
// unique node filter
|
|
||||||
if (item.unique) {
|
|
||||||
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
|
|
||||||
if (nodeExist) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// special node filter
|
|
||||||
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// tool stop or tool params
|
|
||||||
if (
|
|
||||||
!hasToolNode &&
|
|
||||||
(item.flowNodeType === FlowNodeTypeEnum.stopTool ||
|
|
||||||
item.flowNodeType === FlowNodeTypeEnum.toolParams)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map<NodeTemplateListItemType>((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
flowNodeType: item.flowNodeType,
|
|
||||||
templateType: item.templateType,
|
|
||||||
avatar: item.avatar,
|
|
||||||
name: item.name,
|
|
||||||
intro: item.intro
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
manual: false,
|
|
||||||
throttleWait: 100,
|
|
||||||
refreshDeps: [basicNodeTemplates, nodeList, hasToolNode, templateType]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const {
|
const {
|
||||||
data: teamAndSystemApps,
|
templateType,
|
||||||
loading: isLoading,
|
parentId,
|
||||||
runAsync: loadNodeTemplates
|
templatesIsLoading,
|
||||||
} = useRequest2(
|
templates,
|
||||||
async ({
|
loadNodeTemplates,
|
||||||
parentId = '',
|
onUpdateParentId
|
||||||
type = templateType,
|
} = useNodeTemplates();
|
||||||
searchVal = searchKey
|
|
||||||
}: {
|
|
||||||
parentId?: ParentIdType;
|
|
||||||
type?: TemplateTypeEnum;
|
|
||||||
searchVal?: string;
|
|
||||||
}) => {
|
|
||||||
if (type === TemplateTypeEnum.teamPlugin) {
|
|
||||||
return getTeamPlugTemplates({
|
|
||||||
parentId,
|
|
||||||
searchKey: searchVal
|
|
||||||
}).then((res) => res.filter((app) => app.id !== appId));
|
|
||||||
}
|
|
||||||
if (type === TemplateTypeEnum.systemPlugin) {
|
|
||||||
return getSystemPlugTemplates({
|
|
||||||
searchKey: searchVal,
|
|
||||||
parentId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess(res, [{ parentId = '', type = templateType }]) {
|
|
||||||
setParentId(parentId);
|
|
||||||
setTemplateType(type);
|
|
||||||
},
|
|
||||||
refreshDeps: [searchKey, templateType]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const templates = useMemo(
|
const onAddNode = useMemoizedFn(async ({ newNodes }: { newNodes: Node<FlowNodeItemType>[] }) => {
|
||||||
() => basicNodes || teamAndSystemApps || [],
|
setNodes((state) => {
|
||||||
[basicNodes, teamAndSystemApps]
|
const newState = state
|
||||||
);
|
.map((node) => ({
|
||||||
|
...node,
|
||||||
const onUpdateParentId = useCallback(
|
selected: false
|
||||||
(parentId: ParentIdType) => {
|
}))
|
||||||
loadNodeTemplates({
|
// @ts-ignore
|
||||||
parentId
|
.concat(newNodes);
|
||||||
});
|
return newState;
|
||||||
},
|
});
|
||||||
[loadNodeTemplates]
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Init load refresh templates
|
|
||||||
useRequest2(
|
|
||||||
() =>
|
|
||||||
loadNodeTemplates({
|
|
||||||
parentId: '',
|
|
||||||
searchVal: searchKey
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
manual: false,
|
|
||||||
throttleWait: 300,
|
|
||||||
refreshDeps: [searchKey]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -223,7 +57,7 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
|||||||
fontSize={'sm'}
|
fontSize={'sm'}
|
||||||
/>
|
/>
|
||||||
<MyBox
|
<MyBox
|
||||||
isLoading={isLoading}
|
isLoading={templatesIsLoading}
|
||||||
display={'flex'}
|
display={'flex'}
|
||||||
zIndex={3}
|
zIndex={3}
|
||||||
flexDirection={'column'}
|
flexDirection={'column'}
|
||||||
@@ -241,22 +75,18 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
|||||||
userSelect={'none'}
|
userSelect={'none'}
|
||||||
overflow={isOpen ? 'none' : 'hidden'}
|
overflow={isOpen ? 'none' : 'hidden'}
|
||||||
>
|
>
|
||||||
<RenderHeader
|
<NodeTemplateListHeader
|
||||||
templateType={templateType}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
parentId={parentId}
|
templateType={templateType}
|
||||||
onUpdateParentId={onUpdateParentId}
|
|
||||||
searchKey={searchKey}
|
|
||||||
loadNodeTemplates={loadNodeTemplates}
|
loadNodeTemplates={loadNodeTemplates}
|
||||||
setSearchKey={setSearchKey}
|
parentId={parentId || ''}
|
||||||
|
onUpdateParentId={onUpdateParentId}
|
||||||
/>
|
/>
|
||||||
<RenderList
|
<NodeTemplateList
|
||||||
templateType={templateType}
|
onAddNode={onAddNode}
|
||||||
templates={templates}
|
templates={templates}
|
||||||
type={templateType}
|
templateType={templateType}
|
||||||
onClose={onClose}
|
onUpdateParentId={onUpdateParentId}
|
||||||
parentId={parentId}
|
|
||||||
setParentId={onUpdateParentId}
|
|
||||||
/>
|
/>
|
||||||
</MyBox>
|
</MyBox>
|
||||||
</>
|
</>
|
||||||
@@ -264,531 +94,3 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(NodeTemplatesModal);
|
export default React.memo(NodeTemplatesModal);
|
||||||
|
|
||||||
const RenderHeader = React.memo(function RenderHeader({
|
|
||||||
templateType,
|
|
||||||
onClose,
|
|
||||||
parentId,
|
|
||||||
searchKey,
|
|
||||||
setSearchKey,
|
|
||||||
loadNodeTemplates,
|
|
||||||
onUpdateParentId
|
|
||||||
}: RenderHeaderProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { feConfigs } = useSystemStore();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Get paths
|
|
||||||
const { data: paths = [] } = useRequest2(
|
|
||||||
() => {
|
|
||||||
if (templateType === TemplateTypeEnum.teamPlugin)
|
|
||||||
return getAppFolderPath({ sourceId: parentId, type: 'current' });
|
|
||||||
return getSystemPluginPaths({ sourceId: parentId, type: 'current' });
|
|
||||||
},
|
|
||||||
{
|
|
||||||
manual: false,
|
|
||||||
refreshDeps: [parentId]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
|
|
||||||
{/* Tabs */}
|
|
||||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={2}>
|
|
||||||
<Box flex={'1 0 0'}>
|
|
||||||
<FillRowTabs
|
|
||||||
list={[
|
|
||||||
{
|
|
||||||
icon: 'core/modules/basicNode',
|
|
||||||
label: t('common:core.module.template.Basic Node'),
|
|
||||||
value: TemplateTypeEnum.basic
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'phoneTabbar/tool',
|
|
||||||
label: t('common:navbar.Toolkit'),
|
|
||||||
value: TemplateTypeEnum.systemPlugin
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'core/modules/teamPlugin',
|
|
||||||
label: t('common:core.module.template.Team app'),
|
|
||||||
value: TemplateTypeEnum.teamPlugin
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
width={'100%'}
|
|
||||||
py={'5px'}
|
|
||||||
value={templateType}
|
|
||||||
onChange={(e) => {
|
|
||||||
loadNodeTemplates({
|
|
||||||
type: e as TemplateTypeEnum,
|
|
||||||
parentId: ''
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{/* close icon */}
|
|
||||||
<IconButton
|
|
||||||
size={'sm'}
|
|
||||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.600'} />}
|
|
||||||
bg={'myGray.100'}
|
|
||||||
_hover={{
|
|
||||||
bg: 'myGray.200',
|
|
||||||
'& svg': {
|
|
||||||
color: 'primary.600'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={'grayBase'}
|
|
||||||
aria-label={''}
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
{/* Search */}
|
|
||||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
|
||||||
templateType === TemplateTypeEnum.systemPlugin) && (
|
|
||||||
<Flex mt={2} alignItems={'center'} h={10}>
|
|
||||||
<InputGroup mr={4} h={'full'}>
|
|
||||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
|
||||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
|
||||||
</InputLeftElement>
|
|
||||||
<Input
|
|
||||||
h={'full'}
|
|
||||||
bg={'myGray.50'}
|
|
||||||
placeholder={
|
|
||||||
templateType === TemplateTypeEnum.teamPlugin
|
|
||||||
? t('common:plugin.Search_app')
|
|
||||||
: t('common:plugin.Search plugin')
|
|
||||||
}
|
|
||||||
onChange={(e) => setSearchKey(e.target.value)}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
<Box flex={1} />
|
|
||||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
|
||||||
<Flex
|
|
||||||
alignItems={'center'}
|
|
||||||
cursor={'pointer'}
|
|
||||||
_hover={{
|
|
||||||
color: 'primary.600'
|
|
||||||
}}
|
|
||||||
fontSize={'sm'}
|
|
||||||
onClick={() => router.push('/dashboard/apps')}
|
|
||||||
gap={1}
|
|
||||||
>
|
|
||||||
<Box>{t('common:create')}</Box>
|
|
||||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
{templateType === TemplateTypeEnum.systemPlugin && feConfigs.systemPluginCourseUrl && (
|
|
||||||
<Flex
|
|
||||||
alignItems={'center'}
|
|
||||||
cursor={'pointer'}
|
|
||||||
_hover={{
|
|
||||||
color: 'primary.600'
|
|
||||||
}}
|
|
||||||
fontSize={'sm'}
|
|
||||||
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
|
|
||||||
gap={1}
|
|
||||||
>
|
|
||||||
<Box>{t('common:plugin.contribute')}</Box>
|
|
||||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
{/* paths */}
|
|
||||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
|
||||||
templateType === TemplateTypeEnum.systemPlugin) &&
|
|
||||||
!searchKey &&
|
|
||||||
parentId && (
|
|
||||||
<Flex alignItems={'center'} mt={2}>
|
|
||||||
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const RenderList = React.memo(function RenderList({
|
|
||||||
templateType,
|
|
||||||
templates,
|
|
||||||
type,
|
|
||||||
onClose,
|
|
||||||
setParentId
|
|
||||||
}: RenderListProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { setLoading } = useSystemStore();
|
|
||||||
|
|
||||||
const { isPc } = useSystem();
|
|
||||||
|
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
|
||||||
const { computedNewNodeName } = useWorkflowUtils();
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
|
||||||
|
|
||||||
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
|
|
||||||
manual: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatTemplatesArray = useMemo<{ list: NodeTemplateListType; label: string }[]>(() => {
|
|
||||||
const data = (() => {
|
|
||||||
if (type === TemplateTypeEnum.systemPlugin) {
|
|
||||||
return pluginGroups.map((group) => {
|
|
||||||
const copy: NodeTemplateListType = group.groupTypes.map((type) => ({
|
|
||||||
list: [],
|
|
||||||
type: type.typeId,
|
|
||||||
label: type.typeName
|
|
||||||
}));
|
|
||||||
templates.forEach((item) => {
|
|
||||||
const index = copy.findIndex((template) => template.type === item.templateType);
|
|
||||||
if (index === -1) return;
|
|
||||||
copy[index].list.push(item);
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
label: group.groupName,
|
|
||||||
list: copy.filter((item) => item.list.length > 0)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
|
|
||||||
templates.forEach((item) => {
|
|
||||||
const index = copy.findIndex((template) => template.type === item.templateType);
|
|
||||||
if (index === -1) return;
|
|
||||||
copy[index].list.push(item);
|
|
||||||
});
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: '',
|
|
||||||
list: copy.filter((item) => item.list.length > 0)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
})();
|
|
||||||
return data.filter(({ list }) => list.length > 0);
|
|
||||||
}, [type, templates, pluginGroups]);
|
|
||||||
|
|
||||||
const onAddNode = useMemoizedFn(
|
|
||||||
async ({
|
|
||||||
template,
|
|
||||||
position
|
|
||||||
}: {
|
|
||||||
template: NodeTemplateListItemType;
|
|
||||||
position: XYPosition;
|
|
||||||
}) => {
|
|
||||||
// Load template node
|
|
||||||
const templateNode = await (async () => {
|
|
||||||
try {
|
|
||||||
// get plugin preview module
|
|
||||||
if (AppNodeFlowNodeTypeMap[template.flowNodeType]) {
|
|
||||||
setLoading(true);
|
|
||||||
const res = await getPreviewPluginNode({ appId: template.id });
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// base node
|
|
||||||
const baseTemplate = moduleTemplatesFlat.find((item) => item.id === template.id);
|
|
||||||
if (!baseTemplate) {
|
|
||||||
throw new Error('baseTemplate not found');
|
|
||||||
}
|
|
||||||
return { ...baseTemplate };
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
status: 'error',
|
|
||||||
title: getErrText(e, t('common:core.plugin.Get Plugin Module Detail Failed'))
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
return Promise.reject(e);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
const nodePosition = screenToFlowPosition(position);
|
|
||||||
const mouseX = nodePosition.x - 100;
|
|
||||||
const mouseY = nodePosition.y - 20;
|
|
||||||
|
|
||||||
// Add default values to some inputs
|
|
||||||
const defaultValueMap: Record<string, any> = {
|
|
||||||
[NodeInputKeyEnum.userChatInput]: undefined,
|
|
||||||
[NodeInputKeyEnum.fileUrlList]: undefined
|
|
||||||
};
|
|
||||||
nodeList.forEach((node) => {
|
|
||||||
if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) {
|
|
||||||
defaultValueMap[NodeInputKeyEnum.userChatInput] = [
|
|
||||||
node.nodeId,
|
|
||||||
NodeOutputKeyEnum.userChatInput
|
|
||||||
];
|
|
||||||
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
|
|
||||||
[node.nodeId, NodeOutputKeyEnum.userFiles]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const newNode = nodeTemplate2FlowNode({
|
|
||||||
template: {
|
|
||||||
...templateNode,
|
|
||||||
name: computedNewNodeName({
|
|
||||||
templateName: t(templateNode.name as any),
|
|
||||||
flowNodeType: templateNode.flowNodeType,
|
|
||||||
pluginId: templateNode.pluginId
|
|
||||||
}),
|
|
||||||
intro: t(templateNode.intro as any),
|
|
||||||
inputs: templateNode.inputs
|
|
||||||
.filter((input) => input.deprecated !== true)
|
|
||||||
.map((input) => ({
|
|
||||||
...input,
|
|
||||||
value: defaultValueMap[input.key] ?? input.value,
|
|
||||||
valueDesc: t(input.valueDesc as any),
|
|
||||||
label: t(input.label as any),
|
|
||||||
description: t(input.description as any),
|
|
||||||
debugLabel: t(input.debugLabel as any),
|
|
||||||
toolDescription: t(input.toolDescription as any)
|
|
||||||
})),
|
|
||||||
outputs: templateNode.outputs
|
|
||||||
.filter((output) => output.deprecated !== true)
|
|
||||||
.map((output) => ({
|
|
||||||
...output,
|
|
||||||
valueDesc: t(output.valueDesc as any),
|
|
||||||
label: t(output.label as any),
|
|
||||||
description: t(output.description as any)
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
position: { x: mouseX, y: mouseY },
|
|
||||||
selected: true,
|
|
||||||
t
|
|
||||||
});
|
|
||||||
const newNodes = [newNode];
|
|
||||||
|
|
||||||
if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) {
|
|
||||||
const startNode = nodeTemplate2FlowNode({
|
|
||||||
template: LoopStartNode,
|
|
||||||
position: { x: mouseX + 60, y: mouseY + 280 },
|
|
||||||
parentNodeId: newNode.id,
|
|
||||||
t
|
|
||||||
});
|
|
||||||
const endNode = nodeTemplate2FlowNode({
|
|
||||||
template: LoopEndNode,
|
|
||||||
position: { x: mouseX + 420, y: mouseY + 680 },
|
|
||||||
parentNodeId: newNode.id,
|
|
||||||
t
|
|
||||||
});
|
|
||||||
|
|
||||||
newNodes.push(startNode, endNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
setNodes((state) => {
|
|
||||||
const newState = state
|
|
||||||
.map((node) => ({
|
|
||||||
...node,
|
|
||||||
selected: false
|
|
||||||
}))
|
|
||||||
// @ts-ignore
|
|
||||||
.concat(newNodes);
|
|
||||||
return newState;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const gridStyle = useMemo(() => {
|
|
||||||
if (type === TemplateTypeEnum.teamPlugin) {
|
|
||||||
return {
|
|
||||||
gridTemplateColumns: ['1fr', '1fr'],
|
|
||||||
py: 2,
|
|
||||||
avatarSize: '2rem',
|
|
||||||
authorInName: false,
|
|
||||||
authorInRight: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
gridTemplateColumns: ['1fr', '1fr 1fr'],
|
|
||||||
py: 3,
|
|
||||||
avatarSize: '1.75rem',
|
|
||||||
authorInName: true,
|
|
||||||
authorInRight: false
|
|
||||||
};
|
|
||||||
}, [type]);
|
|
||||||
|
|
||||||
const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{list.map((item, i) => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
key={item.type}
|
|
||||||
css={css({
|
|
||||||
span: {
|
|
||||||
display: 'block'
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
|
||||||
{t(item.label as any)}
|
|
||||||
</Box>
|
|
||||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
|
|
||||||
{item.list.map((template) => {
|
|
||||||
return (
|
|
||||||
<MyTooltip
|
|
||||||
key={template.id}
|
|
||||||
placement={'right'}
|
|
||||||
label={
|
|
||||||
<Box py={2}>
|
|
||||||
<Flex alignItems={'center'}>
|
|
||||||
<MyAvatar
|
|
||||||
src={template.avatar}
|
|
||||||
w={'1.75rem'}
|
|
||||||
objectFit={'contain'}
|
|
||||||
borderRadius={'sm'}
|
|
||||||
/>
|
|
||||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
|
||||||
{t(template.name as any)}
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
|
|
||||||
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
|
||||||
</Box>
|
|
||||||
{type === TemplateTypeEnum.systemPlugin && (
|
|
||||||
<CostTooltip
|
|
||||||
cost={template.currentCost}
|
|
||||||
hasTokenFee={template.hasTokenFee}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
alignItems={'center'}
|
|
||||||
py={gridStyle.py}
|
|
||||||
px={3}
|
|
||||||
cursor={'pointer'}
|
|
||||||
_hover={{
|
|
||||||
bg: 'myWhite.600',
|
|
||||||
'& .arrowIcon': {
|
|
||||||
display: 'block'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
borderRadius={'sm'}
|
|
||||||
draggable={
|
|
||||||
!template.isFolder || template.flowNodeType === FlowNodeTypeEnum.toolSet
|
|
||||||
}
|
|
||||||
onDragEnd={(e) => {
|
|
||||||
if (e.clientX < sliderWidth) return;
|
|
||||||
onAddNode({
|
|
||||||
template,
|
|
||||||
position: { x: e.clientX, y: e.clientY }
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (
|
|
||||||
template.isFolder &&
|
|
||||||
template.flowNodeType !== FlowNodeTypeEnum.toolSet
|
|
||||||
) {
|
|
||||||
return setParentId(template.id);
|
|
||||||
}
|
|
||||||
if (isPc) {
|
|
||||||
return onAddNode({
|
|
||||||
template,
|
|
||||||
position: { x: sliderWidth * 1.5, y: 200 }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onAddNode({
|
|
||||||
template,
|
|
||||||
position: { x: e.clientX, y: e.clientY }
|
|
||||||
});
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
whiteSpace={'nowrap'}
|
|
||||||
overflow={'hidden'}
|
|
||||||
textOverflow={'ellipsis'}
|
|
||||||
>
|
|
||||||
<MyAvatar
|
|
||||||
src={template.avatar}
|
|
||||||
w={gridStyle.avatarSize}
|
|
||||||
objectFit={'contain'}
|
|
||||||
borderRadius={'sm'}
|
|
||||||
flexShrink={0}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
color={'myGray.900'}
|
|
||||||
fontWeight={'500'}
|
|
||||||
fontSize={'sm'}
|
|
||||||
flex={'1 0 0'}
|
|
||||||
ml={3}
|
|
||||||
className="textEllipsis"
|
|
||||||
>
|
|
||||||
{t(template.name as any)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{template.isFolder && templateType === TemplateTypeEnum.teamPlugin && (
|
|
||||||
<Box
|
|
||||||
color={'myGray.500'}
|
|
||||||
_hover={{
|
|
||||||
bg: 'var(--light-general-surface-opacity-005, rgba(17, 24, 36, 0.05))',
|
|
||||||
color: 'primary.600'
|
|
||||||
}}
|
|
||||||
p={1}
|
|
||||||
rounded={'sm'}
|
|
||||||
className="arrowIcon"
|
|
||||||
display="none"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
return setParentId(template.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MyIcon name="common/arrowRight" w={'24px'} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{gridStyle.authorInRight && template.authorAvatar && template.author && (
|
|
||||||
<HStack spacing={1} maxW={'120px'} flexShrink={0}>
|
|
||||||
<MyAvatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
|
||||||
<Box fontSize={'xs'} className="textEllipsis">
|
|
||||||
{template.author}
|
|
||||||
</Box>
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</MyTooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return templates.length === 0 ? (
|
|
||||||
<EmptyTip text={t('app:module.No Modules')} />
|
|
||||||
) : (
|
|
||||||
<Box flex={'1 0 0'} overflow={'overlay'} px={formatTemplatesArray.length > 1 ? 2 : 5}>
|
|
||||||
<Accordion defaultIndex={[0]} allowMultiple reduceMotion>
|
|
||||||
{formatTemplatesArray.length > 1 ? (
|
|
||||||
<>
|
|
||||||
{formatTemplatesArray.map(({ list, label }, index) => (
|
|
||||||
<AccordionItem key={index} border={'none'}>
|
|
||||||
<AccordionButton
|
|
||||||
fontSize={'sm'}
|
|
||||||
fontWeight={'500'}
|
|
||||||
color={'myGray.900'}
|
|
||||||
justifyContent={'space-between'}
|
|
||||||
alignItems={'center'}
|
|
||||||
borderRadius={'md'}
|
|
||||||
px={3}
|
|
||||||
>
|
|
||||||
{t(label as any)}
|
|
||||||
<AccordionIcon />
|
|
||||||
</AccordionButton>
|
|
||||||
<AccordionPanel py={0}>
|
|
||||||
<PluginListRender list={list} />
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<PluginListRender list={formatTemplatesArray?.[0]?.list} />
|
|
||||||
)}
|
|
||||||
</Accordion>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
@@ -0,0 +1,137 @@
|
|||||||
|
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useContextSelector } from 'use-context-selector';
|
||||||
|
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
|
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||||
|
import { type Node, useReactFlow } from 'reactflow';
|
||||||
|
import { WorkflowInitContext, WorkflowNodeEdgeContext } from '../context/workflowInitContext';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import NodeTemplateListHeader from './components/NodeTemplates/header';
|
||||||
|
import NodeTemplateList from './components/NodeTemplates/list';
|
||||||
|
import { Popover, PopoverContent, PopoverBody } from '@chakra-ui/react';
|
||||||
|
import { WorkflowEventContext } from '../context/workflowEventContext';
|
||||||
|
import { useNodeTemplates } from './components/NodeTemplates/useNodeTemplates';
|
||||||
|
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||||
|
import { popoverHeight, popoverWidth } from './hooks/useWorkflow';
|
||||||
|
|
||||||
|
const NodeTemplatesPopover = () => {
|
||||||
|
const handleParams = useContextSelector(WorkflowEventContext, (v) => v.handleParams);
|
||||||
|
const setHandleParams = useContextSelector(WorkflowEventContext, (v) => v.setHandleParams);
|
||||||
|
|
||||||
|
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
||||||
|
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
|
||||||
|
|
||||||
|
const {
|
||||||
|
templateType,
|
||||||
|
parentId,
|
||||||
|
templatesIsLoading,
|
||||||
|
templates,
|
||||||
|
loadNodeTemplates,
|
||||||
|
onUpdateParentId
|
||||||
|
} = useNodeTemplates();
|
||||||
|
|
||||||
|
const onAddNode = useMemoizedFn(async ({ newNodes }: { newNodes: Node<FlowNodeItemType>[] }) => {
|
||||||
|
setNodes((state) => {
|
||||||
|
const newState = state
|
||||||
|
.map((node) => ({
|
||||||
|
...node,
|
||||||
|
selected: false
|
||||||
|
}))
|
||||||
|
// @ts-ignore
|
||||||
|
.concat(newNodes);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!handleParams) return;
|
||||||
|
const isToolHandle = handleParams?.handleId === 'selectedTools';
|
||||||
|
|
||||||
|
const newEdges = newNodes
|
||||||
|
.filter((node) => {
|
||||||
|
// Exclude nodes that don't meet the conditions
|
||||||
|
// 1. Tool set nodes must be connected through tool handle
|
||||||
|
if (!isToolHandle && node.data.flowNodeType === FlowNodeTypeEnum.toolSet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Exclude loop start and end nodes
|
||||||
|
if (
|
||||||
|
[FlowNodeTypeEnum.loopStart, FlowNodeTypeEnum.loopEnd].includes(node.data.flowNodeType)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Tool handle can only connect to tool nodes
|
||||||
|
if (isToolHandle && !node.data.isTool) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((node) => ({
|
||||||
|
id: getNanoid(),
|
||||||
|
source: handleParams.nodeId as string,
|
||||||
|
sourceHandle: handleParams.handleId,
|
||||||
|
target: node.id,
|
||||||
|
targetHandle: isToolHandle ? 'selectedTools' : `${node.id}-target-left`,
|
||||||
|
type: EDGE_TYPE
|
||||||
|
}));
|
||||||
|
|
||||||
|
setEdges((state) => {
|
||||||
|
const newState = state.concat(newEdges);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
|
||||||
|
setHandleParams(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!handleParams) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
isOpen={!!handleParams}
|
||||||
|
onClose={() => setHandleParams(null)}
|
||||||
|
closeOnBlur={true}
|
||||||
|
closeOnEsc={true}
|
||||||
|
autoFocus={true}
|
||||||
|
isLazy
|
||||||
|
>
|
||||||
|
<PopoverContent
|
||||||
|
position="fixed"
|
||||||
|
top={`${handleParams.popoverPosition.y}px`}
|
||||||
|
left={`${handleParams.popoverPosition.x + 10}px`}
|
||||||
|
width={popoverWidth}
|
||||||
|
height={popoverHeight}
|
||||||
|
boxShadow="3px 0 20px rgba(0,0,0,0.2)"
|
||||||
|
border={'none'}
|
||||||
|
>
|
||||||
|
<PopoverBody padding={0} h={'full'}>
|
||||||
|
<MyBox
|
||||||
|
isLoading={templatesIsLoading}
|
||||||
|
display={'flex'}
|
||||||
|
flexDirection={'column'}
|
||||||
|
py={4}
|
||||||
|
h={'full'}
|
||||||
|
userSelect="none"
|
||||||
|
>
|
||||||
|
<NodeTemplateListHeader
|
||||||
|
isPopover={true}
|
||||||
|
templateType={templateType}
|
||||||
|
loadNodeTemplates={loadNodeTemplates}
|
||||||
|
parentId={parentId || ''}
|
||||||
|
onUpdateParentId={onUpdateParentId}
|
||||||
|
/>
|
||||||
|
<NodeTemplateList
|
||||||
|
onAddNode={onAddNode}
|
||||||
|
isPopover={true}
|
||||||
|
templates={templates}
|
||||||
|
templateType={templateType}
|
||||||
|
onUpdateParentId={onUpdateParentId}
|
||||||
|
/>
|
||||||
|
</MyBox>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NodeTemplatesPopover);
|
@@ -93,24 +93,6 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
newTargetY: targetY
|
newTargetY: targetY
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (targetPosition === 'right') {
|
|
||||||
return {
|
|
||||||
newTargetX: targetX + 3,
|
|
||||||
newTargetY: targetY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (targetPosition === 'bottom') {
|
|
||||||
return {
|
|
||||||
newTargetX: targetX,
|
|
||||||
newTargetY: targetY + 3
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (targetPosition === 'top') {
|
|
||||||
return {
|
|
||||||
newTargetX: targetX,
|
|
||||||
newTargetY: targetY - 3
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
newTargetX: targetX,
|
newTargetX: targetX,
|
||||||
newTargetY: targetY
|
newTargetY: targetY
|
||||||
@@ -150,6 +132,7 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
return `translate(-50%, -90%) translate(${newTargetX}px,${newTargetY}px) rotate(90deg)`;
|
return `translate(-50%, -90%) translate(${newTargetX}px,${newTargetY}px) rotate(90deg)`;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<Box hidden={parentNode?.isFolded}>
|
<Box hidden={parentNode?.isFolded}>
|
||||||
@@ -160,8 +143,8 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
position={'absolute'}
|
position={'absolute'}
|
||||||
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
|
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||||
pointerEvents={'all'}
|
pointerEvents={'all'}
|
||||||
w={'18px'}
|
w={'26px'}
|
||||||
h={'18px'}
|
h={'26px'}
|
||||||
bg={'white'}
|
bg={'white'}
|
||||||
borderRadius={'18px'}
|
borderRadius={'18px'}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
@@ -177,8 +160,8 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
position={'absolute'}
|
position={'absolute'}
|
||||||
transform={arrowTransform}
|
transform={arrowTransform}
|
||||||
pointerEvents={'all'}
|
pointerEvents={'all'}
|
||||||
w={highlightEdge ? '12px' : '10px'}
|
w={highlightEdge ? '14px' : '12px'}
|
||||||
h={highlightEdge ? '12px' : '10px'}
|
h={highlightEdge ? '14px' : '12px'}
|
||||||
zIndex={highlightEdge ? defaultZIndex + 1000 : defaultZIndex}
|
zIndex={highlightEdge ? defaultZIndex + 1000 : defaultZIndex}
|
||||||
>
|
>
|
||||||
<MyIcon
|
<MyIcon
|
||||||
@@ -197,8 +180,8 @@ const ButtonEdge = (props: EdgeProps) => {
|
|||||||
highlightEdge,
|
highlightEdge,
|
||||||
labelX,
|
labelX,
|
||||||
labelY,
|
labelY,
|
||||||
isToolEdge,
|
|
||||||
defaultZIndex,
|
defaultZIndex,
|
||||||
|
isToolEdge,
|
||||||
edgeColor,
|
edgeColor,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
newTargetX,
|
newTargetX,
|
||||||
|
@@ -0,0 +1,200 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Box, Flex, IconButton, Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
|
||||||
|
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||||
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
|
import { getSystemPluginPaths } from '@/web/core/app/api/plugin';
|
||||||
|
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||||
|
import FolderPath from '@/components/common/folder/Path';
|
||||||
|
|
||||||
|
export enum TemplateTypeEnum {
|
||||||
|
'basic' = 'basic',
|
||||||
|
'systemPlugin' = 'systemPlugin',
|
||||||
|
'teamPlugin' = 'teamPlugin'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeTemplateListHeaderProps = {
|
||||||
|
onClose?: () => void;
|
||||||
|
isPopover?: boolean;
|
||||||
|
templateType: TemplateTypeEnum;
|
||||||
|
parentId: string;
|
||||||
|
loadNodeTemplates: (params: {
|
||||||
|
parentId?: string;
|
||||||
|
type?: TemplateTypeEnum;
|
||||||
|
searchVal?: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
onUpdateParentId: (parentId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeTemplateListHeader = ({
|
||||||
|
onClose,
|
||||||
|
isPopover = false,
|
||||||
|
templateType,
|
||||||
|
parentId,
|
||||||
|
loadNodeTemplates,
|
||||||
|
onUpdateParentId
|
||||||
|
}: NodeTemplateListHeaderProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { feConfigs } = useSystemStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [searchKey, setSearchKey] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchKey('');
|
||||||
|
}, [templateType]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadNodeTemplates({
|
||||||
|
type: templateType,
|
||||||
|
parentId: '',
|
||||||
|
searchVal: searchKey
|
||||||
|
});
|
||||||
|
}, [searchKey, loadNodeTemplates, templateType]);
|
||||||
|
|
||||||
|
// Get paths
|
||||||
|
const { data: paths = [] } = useRequest2(
|
||||||
|
() => {
|
||||||
|
if (templateType === TemplateTypeEnum.teamPlugin)
|
||||||
|
return getAppFolderPath({ sourceId: parentId, type: 'current' });
|
||||||
|
return getSystemPluginPaths({ sourceId: parentId, type: 'current' });
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manual: false,
|
||||||
|
refreshDeps: [parentId]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||||
|
{/* Tabs */}
|
||||||
|
<Flex flex={'1 0 0'} alignItems={'center'} gap={2}>
|
||||||
|
<Box flex={'1 0 0'}>
|
||||||
|
<FillRowTabs
|
||||||
|
list={[
|
||||||
|
{
|
||||||
|
icon: 'core/modules/basicNode',
|
||||||
|
label: t('common:core.module.template.Basic Node'),
|
||||||
|
value: TemplateTypeEnum.basic
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'phoneTabbar/tool',
|
||||||
|
label: t('common:navbar.Toolkit'),
|
||||||
|
value: TemplateTypeEnum.systemPlugin
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'core/modules/teamPlugin',
|
||||||
|
label: t('common:core.module.template.Team app'),
|
||||||
|
value: TemplateTypeEnum.teamPlugin
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
width={'100%'}
|
||||||
|
py={isPopover ? '3px' : '5px'}
|
||||||
|
{...(isPopover
|
||||||
|
? {
|
||||||
|
iconSize: '14px',
|
||||||
|
labelSize: '12.8px',
|
||||||
|
iconGap: 1
|
||||||
|
}
|
||||||
|
: {})}
|
||||||
|
value={templateType}
|
||||||
|
onChange={(e) => {
|
||||||
|
loadNodeTemplates({
|
||||||
|
type: e as TemplateTypeEnum,
|
||||||
|
parentId: ''
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{/* close icon */}
|
||||||
|
{!isPopover && (
|
||||||
|
<IconButton
|
||||||
|
size={'sm'}
|
||||||
|
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.600'} />}
|
||||||
|
bg={'myGray.100'}
|
||||||
|
_hover={{
|
||||||
|
bg: 'myGray.200',
|
||||||
|
'& svg': {
|
||||||
|
color: 'primary.600'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant={'grayBase'}
|
||||||
|
aria-label={''}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{/* Search */}
|
||||||
|
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||||
|
templateType === TemplateTypeEnum.systemPlugin) && (
|
||||||
|
<Flex mt={2} alignItems={'center'} h={isPopover ? 8 : 10}>
|
||||||
|
<InputGroup h={'full'}>
|
||||||
|
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||||
|
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
h={'full'}
|
||||||
|
bg={'myGray.50'}
|
||||||
|
placeholder={
|
||||||
|
templateType === TemplateTypeEnum.teamPlugin
|
||||||
|
? t('common:plugin.Search_app')
|
||||||
|
: t('common:plugin.Search plugin')
|
||||||
|
}
|
||||||
|
value={searchKey}
|
||||||
|
onChange={(e) => setSearchKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
<Box flex={1} />
|
||||||
|
{!isPopover && templateType === TemplateTypeEnum.teamPlugin && (
|
||||||
|
<Flex
|
||||||
|
alignItems={'center'}
|
||||||
|
cursor={'pointer'}
|
||||||
|
_hover={{
|
||||||
|
color: 'primary.600'
|
||||||
|
}}
|
||||||
|
fontSize={'sm'}
|
||||||
|
onClick={() => router.push('/dashboard/apps')}
|
||||||
|
gap={1}
|
||||||
|
ml={4}
|
||||||
|
>
|
||||||
|
<Box>{t('common:create')}</Box>
|
||||||
|
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{!isPopover &&
|
||||||
|
templateType === TemplateTypeEnum.systemPlugin &&
|
||||||
|
feConfigs.systemPluginCourseUrl && (
|
||||||
|
<Flex
|
||||||
|
alignItems={'center'}
|
||||||
|
cursor={'pointer'}
|
||||||
|
_hover={{
|
||||||
|
color: 'primary.600'
|
||||||
|
}}
|
||||||
|
fontSize={'sm'}
|
||||||
|
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
|
||||||
|
gap={1}
|
||||||
|
ml={4}
|
||||||
|
>
|
||||||
|
<Box>{t('common:plugin.contribute')}</Box>
|
||||||
|
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{/* paths */}
|
||||||
|
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||||
|
templateType === TemplateTypeEnum.systemPlugin) &&
|
||||||
|
!searchKey &&
|
||||||
|
parentId && (
|
||||||
|
<Flex alignItems={'center'} mt={2}>
|
||||||
|
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NodeTemplateListHeader);
|
@@ -0,0 +1,461 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionButton,
|
||||||
|
AccordionIcon,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionPanel,
|
||||||
|
Box,
|
||||||
|
Grid,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
css,
|
||||||
|
useToast
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
|
import { getPluginGroups, getPreviewPluginNode } from '@/web/core/app/api/plugin';
|
||||||
|
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||||
|
import type {
|
||||||
|
FlowNodeItemType,
|
||||||
|
NodeTemplateListItemType,
|
||||||
|
NodeTemplateListType
|
||||||
|
} from '@fastgpt/global/core/workflow/type/node';
|
||||||
|
import { TemplateTypeEnum } from './header';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
|
import MyAvatar from '@fastgpt/web/components/common/Avatar';
|
||||||
|
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||||
|
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
|
||||||
|
import {
|
||||||
|
FlowNodeTypeEnum,
|
||||||
|
AppNodeFlowNodeTypeMap
|
||||||
|
} from '@fastgpt/global/core/workflow/node/constant';
|
||||||
|
import { useContextSelector } from 'use-context-selector';
|
||||||
|
import { WorkflowContext } from '../../../context';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { workflowNodeTemplateList } from '@fastgpt/web/core/workflow/constants';
|
||||||
|
import { sliderWidth } from '../../NodeTemplatesModal';
|
||||||
|
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||||
|
import { useWorkflowUtils } from '../../hooks/useUtils';
|
||||||
|
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||||
|
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
||||||
|
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
||||||
|
import { useReactFlow, type Node } from 'reactflow';
|
||||||
|
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
|
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
|
||||||
|
import { WorkflowEventContext } from '../../../context/workflowEventContext';
|
||||||
|
|
||||||
|
export type TemplateListProps = {
|
||||||
|
onAddNode: ({ newNodes }: { newNodes: Node<FlowNodeItemType>[] }) => void;
|
||||||
|
isPopover?: boolean;
|
||||||
|
templates: NodeTemplateListItemType[];
|
||||||
|
templateType: TemplateTypeEnum;
|
||||||
|
onUpdateParentId: (parentId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeTemplateListItem = ({
|
||||||
|
template,
|
||||||
|
templateType,
|
||||||
|
handleAddNode,
|
||||||
|
isPopover,
|
||||||
|
onUpdateParentId
|
||||||
|
}: {
|
||||||
|
template: NodeTemplateListItemType;
|
||||||
|
templateType: TemplateTypeEnum;
|
||||||
|
handleAddNode: (e: {
|
||||||
|
template: NodeTemplateListItemType;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
}) => void;
|
||||||
|
isPopover?: boolean;
|
||||||
|
onUpdateParentId: (parentId: string) => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
|
const handleParams = useContextSelector(WorkflowEventContext, (v) => v.handleParams);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyTooltip
|
||||||
|
placement={'right'}
|
||||||
|
label={
|
||||||
|
<Box py={2}>
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
<MyAvatar
|
||||||
|
src={template.avatar}
|
||||||
|
w={'1.75rem'}
|
||||||
|
objectFit={'contain'}
|
||||||
|
borderRadius={'sm'}
|
||||||
|
/>
|
||||||
|
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
||||||
|
{t(template.name as any)}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
|
||||||
|
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
||||||
|
</Box>
|
||||||
|
{templateType === TemplateTypeEnum.systemPlugin && (
|
||||||
|
<CostTooltip cost={template.currentCost} hasTokenFee={template.hasTokenFee} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
shouldWrapChildren={false}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
w={'100%'}
|
||||||
|
alignItems={'center'}
|
||||||
|
py={isPopover ? 2 : 3}
|
||||||
|
px={isPopover ? 2 : 3}
|
||||||
|
cursor={'pointer'}
|
||||||
|
_hover={{
|
||||||
|
bg: 'myWhite.600',
|
||||||
|
'& .arrowIcon': {
|
||||||
|
display: 'flex'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
borderRadius={'sm'}
|
||||||
|
whiteSpace={'nowrap'}
|
||||||
|
overflow={'hidden'}
|
||||||
|
textOverflow={'ellipsis'}
|
||||||
|
draggable={
|
||||||
|
!isPopover && (!template.isFolder || template.flowNodeType === FlowNodeTypeEnum.toolSet)
|
||||||
|
}
|
||||||
|
onDragEnd={(e) => {
|
||||||
|
if (e.clientX < sliderWidth) return;
|
||||||
|
const nodePosition = screenToFlowPosition({ x: e.clientX, y: e.clientY });
|
||||||
|
handleAddNode({
|
||||||
|
template,
|
||||||
|
position: { x: nodePosition.x - 100, y: nodePosition.y - 20 }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (template.isFolder && template.flowNodeType !== FlowNodeTypeEnum.toolSet) {
|
||||||
|
onUpdateParentId(template.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position =
|
||||||
|
isPopover && handleParams
|
||||||
|
? handleParams.addNodePosition
|
||||||
|
: screenToFlowPosition({ x: sliderWidth * 1.5, y: 200 });
|
||||||
|
|
||||||
|
handleAddNode({ template, position });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MyAvatar
|
||||||
|
src={template.avatar}
|
||||||
|
w={isPopover ? '1.5rem' : '1.75rem'}
|
||||||
|
objectFit={'contain'}
|
||||||
|
borderRadius={'sm'}
|
||||||
|
flexShrink={0}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
color={'myGray.900'}
|
||||||
|
fontWeight={'500'}
|
||||||
|
fontSize={isPopover ? 'xs' : 'sm'}
|
||||||
|
flex={'1 0 0'}
|
||||||
|
ml={3}
|
||||||
|
className="textEllipsis"
|
||||||
|
>
|
||||||
|
{t(template.name as any)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Folder right arrow */}
|
||||||
|
{template.isFolder && templateType === TemplateTypeEnum.teamPlugin && (
|
||||||
|
<Box
|
||||||
|
color={'myGray.500'}
|
||||||
|
_hover={{
|
||||||
|
bg: 'var(--light-general-surface-opacity-005, rgba(17, 24, 36, 0.05))',
|
||||||
|
color: 'primary.600'
|
||||||
|
}}
|
||||||
|
p={1}
|
||||||
|
rounded={'sm'}
|
||||||
|
className="arrowIcon"
|
||||||
|
display="none"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onUpdateParentId(template.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MyIcon name="common/arrowRight" w={isPopover ? '16px' : '20px'} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Author */}
|
||||||
|
{!isPopover && template.authorAvatar && template.author && (
|
||||||
|
<HStack spacing={1} maxW={'120px'} flexShrink={0}>
|
||||||
|
<MyAvatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
||||||
|
<Box fontSize={'xs'} className="textEllipsis">
|
||||||
|
{template.author}
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</MyTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeTemplateList = ({
|
||||||
|
onAddNode,
|
||||||
|
isPopover = false,
|
||||||
|
templates,
|
||||||
|
templateType,
|
||||||
|
onUpdateParentId
|
||||||
|
}: TemplateListProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const { computedNewNodeName } = useWorkflowUtils();
|
||||||
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
|
const handleParams = useContextSelector(WorkflowEventContext, (v) => v.handleParams);
|
||||||
|
|
||||||
|
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
|
||||||
|
manual: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddNode = useMemoizedFn(
|
||||||
|
async ({
|
||||||
|
template,
|
||||||
|
position
|
||||||
|
}: {
|
||||||
|
template: NodeTemplateListItemType;
|
||||||
|
position: { x: number; y: number };
|
||||||
|
}) => {
|
||||||
|
if (template.isFolder && template.flowNodeType !== FlowNodeTypeEnum.toolSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const templateNode = await (async () => {
|
||||||
|
try {
|
||||||
|
if (AppNodeFlowNodeTypeMap[template.flowNodeType]) {
|
||||||
|
const res = await getPreviewPluginNode({ appId: template.id });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseTemplate = moduleTemplatesFlat.find((item) => item.id === template.id);
|
||||||
|
if (!baseTemplate) {
|
||||||
|
throw new Error('baseTemplate not found');
|
||||||
|
}
|
||||||
|
return { ...baseTemplate };
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
status: 'error',
|
||||||
|
title: getErrText(e, t('common:core.plugin.Get Plugin Module Detail Failed'))
|
||||||
|
});
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const defaultValueMap: Record<string, any> = {
|
||||||
|
[NodeInputKeyEnum.userChatInput]: undefined,
|
||||||
|
[NodeInputKeyEnum.fileUrlList]: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeList.forEach((node) => {
|
||||||
|
if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) {
|
||||||
|
defaultValueMap[NodeInputKeyEnum.userChatInput] = [
|
||||||
|
node.nodeId,
|
||||||
|
NodeOutputKeyEnum.userChatInput
|
||||||
|
];
|
||||||
|
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
|
||||||
|
[node.nodeId, NodeOutputKeyEnum.userFiles]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentNode = nodeList.find((node) => node.nodeId === handleParams?.nodeId);
|
||||||
|
|
||||||
|
const newNode = nodeTemplate2FlowNode({
|
||||||
|
template: {
|
||||||
|
...templateNode,
|
||||||
|
name: computedNewNodeName({
|
||||||
|
templateName: t(templateNode.name as any),
|
||||||
|
flowNodeType: templateNode.flowNodeType,
|
||||||
|
pluginId: templateNode.pluginId
|
||||||
|
}),
|
||||||
|
intro: t(templateNode.intro as any),
|
||||||
|
inputs: templateNode.inputs
|
||||||
|
.filter((input) => input.deprecated !== true)
|
||||||
|
.map((input) => ({
|
||||||
|
...input,
|
||||||
|
value: defaultValueMap[input.key] ?? input.value,
|
||||||
|
valueDesc: t(input.valueDesc as any),
|
||||||
|
label: t(input.label as any),
|
||||||
|
description: t(input.description as any),
|
||||||
|
debugLabel: t(input.debugLabel as any),
|
||||||
|
toolDescription: t(input.toolDescription as any)
|
||||||
|
})),
|
||||||
|
outputs: templateNode.outputs
|
||||||
|
.filter((output) => output.deprecated !== true)
|
||||||
|
.map((output) => ({
|
||||||
|
...output,
|
||||||
|
valueDesc: t(output.valueDesc as any),
|
||||||
|
label: t(output.label as any),
|
||||||
|
description: t(output.description as any)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
position,
|
||||||
|
selected: true,
|
||||||
|
parentNodeId: currentNode?.parentNodeId,
|
||||||
|
t
|
||||||
|
});
|
||||||
|
|
||||||
|
const newNodes = [newNode];
|
||||||
|
|
||||||
|
if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) {
|
||||||
|
const startNode = nodeTemplate2FlowNode({
|
||||||
|
template: LoopStartNode,
|
||||||
|
position: { x: position.x + 60, y: position.y + 280 },
|
||||||
|
parentNodeId: newNode.id,
|
||||||
|
t
|
||||||
|
});
|
||||||
|
const endNode = nodeTemplate2FlowNode({
|
||||||
|
template: LoopEndNode,
|
||||||
|
position: { x: position.x + 420, y: position.y + 680 },
|
||||||
|
parentNodeId: newNode.id,
|
||||||
|
t
|
||||||
|
});
|
||||||
|
|
||||||
|
newNodes.push(startNode, endNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newNodes && newNodes.length > 0) {
|
||||||
|
onAddNode({
|
||||||
|
newNodes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create node template:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatTemplatesArray = useMemoizedFn(
|
||||||
|
(
|
||||||
|
type: TemplateTypeEnum,
|
||||||
|
templates: NodeTemplateListItemType[],
|
||||||
|
pluginGroups: any[]
|
||||||
|
): { list: NodeTemplateListType; label: string }[] => {
|
||||||
|
const data = (() => {
|
||||||
|
if (type === TemplateTypeEnum.systemPlugin) {
|
||||||
|
return pluginGroups.map((group) => {
|
||||||
|
const copy: NodeTemplateListType = group.groupTypes.map((type: any) => ({
|
||||||
|
list: [],
|
||||||
|
type: type.typeId,
|
||||||
|
label: type.typeName
|
||||||
|
}));
|
||||||
|
templates.forEach((item) => {
|
||||||
|
const index = copy.findIndex((template) => template.type === item.templateType);
|
||||||
|
if (index === -1) return;
|
||||||
|
copy[index].list.push(item);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
label: group.groupName,
|
||||||
|
list: copy.filter((item) => item.list.length > 0)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
|
||||||
|
templates.forEach((item) => {
|
||||||
|
const index = copy.findIndex((template) => template.type === item.templateType);
|
||||||
|
if (index === -1) return;
|
||||||
|
copy[index].list.push(item);
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
list: copy.filter((item) => item.list.length > 0)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
})();
|
||||||
|
return data.filter(({ list }) => list.length > 0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatTemplatesArrayData = useMemo(
|
||||||
|
() => formatTemplatesArray(templateType, templates, pluginGroups),
|
||||||
|
[templateType, templates, pluginGroups, formatTemplatesArray]
|
||||||
|
);
|
||||||
|
|
||||||
|
const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{list.map((item) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={item.type}
|
||||||
|
css={css({
|
||||||
|
span: {
|
||||||
|
display: 'block'
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
fontSize={isPopover ? '12.8px' : 'sm'}
|
||||||
|
my={2}
|
||||||
|
fontWeight={'500'}
|
||||||
|
flex={1}
|
||||||
|
color={isPopover ? 'myGray.600' : 'myGray.900'}
|
||||||
|
>
|
||||||
|
{t(item.label as any)}
|
||||||
|
</Box>
|
||||||
|
<Grid
|
||||||
|
gridTemplateColumns={
|
||||||
|
templateType === TemplateTypeEnum.teamPlugin ? ['1fr'] : ['1fr', '1fr 1fr']
|
||||||
|
}
|
||||||
|
rowGap={2}
|
||||||
|
>
|
||||||
|
{item.list.map((template) => (
|
||||||
|
<NodeTemplateListItem
|
||||||
|
key={template.id}
|
||||||
|
template={template}
|
||||||
|
templateType={templateType}
|
||||||
|
handleAddNode={handleAddNode}
|
||||||
|
isPopover={isPopover}
|
||||||
|
onUpdateParentId={onUpdateParentId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return templates.length === 0 ? (
|
||||||
|
<EmptyTip text={t('app:module.No Modules')} />
|
||||||
|
) : (
|
||||||
|
<Box flex={'1 0 0'} overflow={'overlay'} px={formatTemplatesArrayData.length > 1 ? 2 : 5}>
|
||||||
|
<Accordion defaultIndex={[0]} allowMultiple reduceMotion>
|
||||||
|
{formatTemplatesArrayData.length > 1 ? (
|
||||||
|
<>
|
||||||
|
{formatTemplatesArrayData.map(({ list, label }, index) => (
|
||||||
|
<AccordionItem key={index} border={'none'}>
|
||||||
|
<AccordionButton
|
||||||
|
fontSize={'sm'}
|
||||||
|
fontWeight={'500'}
|
||||||
|
color={'myGray.900'}
|
||||||
|
justifyContent={'space-between'}
|
||||||
|
alignItems={'center'}
|
||||||
|
borderRadius={'md'}
|
||||||
|
px={3}
|
||||||
|
>
|
||||||
|
{t(label as any)}
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel py={0}>
|
||||||
|
<PluginListRender list={list} />
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<PluginListRender list={formatTemplatesArrayData?.[0]?.list} />
|
||||||
|
)}
|
||||||
|
</Accordion>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NodeTemplateList);
|
@@ -0,0 +1,136 @@
|
|||||||
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
|
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||||
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
|
import type { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||||
|
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
|
import { getTeamPlugTemplates, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
|
||||||
|
import { TemplateTypeEnum } from './header';
|
||||||
|
import { useContextSelector } from 'use-context-selector';
|
||||||
|
import { WorkflowContext } from '../../../context';
|
||||||
|
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||||
|
|
||||||
|
export const useNodeTemplates = () => {
|
||||||
|
const { feConfigs } = useSystemStore();
|
||||||
|
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
|
||||||
|
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||||
|
|
||||||
|
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
|
||||||
|
const appId = useContextSelector(WorkflowContext, (state) => state.appId || '');
|
||||||
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
|
|
||||||
|
const hasToolNode = useMemo(
|
||||||
|
() => nodeList.some((node) => node.flowNodeType === FlowNodeTypeEnum.toolSet),
|
||||||
|
[nodeList]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: basicNodes } = useRequest2(
|
||||||
|
async () => {
|
||||||
|
if (templateType === TemplateTypeEnum.basic) {
|
||||||
|
return basicNodeTemplates
|
||||||
|
.filter((item) => {
|
||||||
|
// unique node filter
|
||||||
|
if (item.unique) {
|
||||||
|
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
|
||||||
|
if (nodeExist) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// special node filter
|
||||||
|
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// tool stop or tool params
|
||||||
|
if (
|
||||||
|
!hasToolNode &&
|
||||||
|
(item.flowNodeType === FlowNodeTypeEnum.stopTool ||
|
||||||
|
item.flowNodeType === FlowNodeTypeEnum.toolParams)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map<NodeTemplateListItemType>((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
flowNodeType: item.flowNodeType,
|
||||||
|
templateType: item.templateType,
|
||||||
|
avatar: item.avatar,
|
||||||
|
name: item.name,
|
||||||
|
intro: item.intro
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manual: false,
|
||||||
|
throttleWait: 100,
|
||||||
|
refreshDeps: [basicNodeTemplates, nodeList, hasToolNode, templateType]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: teamAndSystemApps,
|
||||||
|
loading: templatesIsLoading,
|
||||||
|
runAsync
|
||||||
|
} = useRequest2(
|
||||||
|
async ({
|
||||||
|
parentId = '',
|
||||||
|
type = templateType,
|
||||||
|
searchVal = ''
|
||||||
|
}: {
|
||||||
|
parentId?: ParentIdType;
|
||||||
|
type?: TemplateTypeEnum;
|
||||||
|
searchVal?: string;
|
||||||
|
}) => {
|
||||||
|
if (type === TemplateTypeEnum.teamPlugin) {
|
||||||
|
return getTeamPlugTemplates({
|
||||||
|
parentId,
|
||||||
|
searchKey: searchVal
|
||||||
|
}).then((res) => res.filter((app) => app.id !== appId));
|
||||||
|
}
|
||||||
|
if (type === TemplateTypeEnum.systemPlugin) {
|
||||||
|
return getSystemPlugTemplates({
|
||||||
|
searchKey: searchVal,
|
||||||
|
parentId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(res, [{ parentId = '', type = templateType }]) {
|
||||||
|
setParentId(parentId);
|
||||||
|
setTemplateType(type);
|
||||||
|
},
|
||||||
|
refreshDeps: [templateType]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadNodeTemplates = useCallback(
|
||||||
|
async (params: { parentId?: ParentIdType; type?: TemplateTypeEnum; searchVal?: string }) => {
|
||||||
|
await runAsync(params);
|
||||||
|
},
|
||||||
|
[runAsync]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onUpdateParentId = useCallback(
|
||||||
|
(parentId: ParentIdType) => {
|
||||||
|
loadNodeTemplates({
|
||||||
|
parentId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[loadNodeTemplates]
|
||||||
|
);
|
||||||
|
|
||||||
|
const templates = useMemo(() => {
|
||||||
|
if (templateType === TemplateTypeEnum.basic) {
|
||||||
|
return basicNodes || [];
|
||||||
|
}
|
||||||
|
return teamAndSystemApps || [];
|
||||||
|
}, [basicNodes, teamAndSystemApps, templateType]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
templateType,
|
||||||
|
parentId,
|
||||||
|
templatesIsLoading,
|
||||||
|
templates,
|
||||||
|
loadNodeTemplates,
|
||||||
|
onUpdateParentId
|
||||||
|
};
|
||||||
|
};
|
@@ -266,6 +266,9 @@ const computeHelperLines = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const popoverWidth = 400;
|
||||||
|
export const popoverHeight = 600;
|
||||||
|
|
||||||
export const useWorkflow = () => {
|
export const useWorkflow = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -289,8 +292,9 @@ export const useWorkflow = () => {
|
|||||||
WorkflowStatusContext,
|
WorkflowStatusContext,
|
||||||
(v) => v.resetParentNodeSizeAndPosition
|
(v) => v.resetParentNodeSizeAndPosition
|
||||||
);
|
);
|
||||||
|
const setHandleParams = useContextSelector(WorkflowEventContext, (v) => v.setHandleParams);
|
||||||
|
|
||||||
const { getIntersectingNodes } = useReactFlow();
|
const { getIntersectingNodes, flowToScreenPosition, getZoom } = useReactFlow();
|
||||||
const { isDowningCtrl } = useKeyboard();
|
const { isDowningCtrl } = useKeyboard();
|
||||||
|
|
||||||
/* helper line */
|
/* helper line */
|
||||||
@@ -375,6 +379,61 @@ export const useWorkflow = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getTemplatesListPopoverPosition = useMemoizedFn(({ nodeId }: { nodeId: string | null }) => {
|
||||||
|
const node = nodes.find((n) => n.id === nodeId);
|
||||||
|
if (!node) return { x: 0, y: 0 };
|
||||||
|
|
||||||
|
const position = flowToScreenPosition({
|
||||||
|
x: node.position.x,
|
||||||
|
y: node.position.y
|
||||||
|
});
|
||||||
|
|
||||||
|
const zoom = getZoom();
|
||||||
|
|
||||||
|
let x = position.x + (node.width || 0) * zoom;
|
||||||
|
let y = position.y;
|
||||||
|
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
const margin = 20;
|
||||||
|
|
||||||
|
// Check right boundary
|
||||||
|
if (x + popoverWidth + margin > viewportWidth) {
|
||||||
|
x = Math.max(margin, position.x + (node.width || 0) * zoom - popoverWidth - 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bottom boundary
|
||||||
|
if (y + popoverHeight + margin > viewportHeight) {
|
||||||
|
y = Math.max(margin, viewportHeight - popoverHeight - margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check top boundary
|
||||||
|
if (y < margin) {
|
||||||
|
y = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
});
|
||||||
|
const getAddNodePosition = useMemoizedFn(
|
||||||
|
({ nodeId, handleId }: { nodeId: string | null; handleId: string | null }) => {
|
||||||
|
const node = nodes.find((n) => n.id === nodeId);
|
||||||
|
if (!node) return { x: 0, y: 0 };
|
||||||
|
|
||||||
|
if (handleId === 'selectedTools') {
|
||||||
|
return {
|
||||||
|
x: node.position.x,
|
||||||
|
y: node.position.y + (node.height || 0) + 80
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: node.position.x + (node.width || 0) + 120,
|
||||||
|
y: node.position.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/* node */
|
/* node */
|
||||||
// Remove change node and its child nodes and edges
|
// Remove change node and its child nodes and edges
|
||||||
const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, nodeId: string) => {
|
const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, nodeId: string) => {
|
||||||
@@ -525,21 +584,61 @@ export const useWorkflow = () => {
|
|||||||
/* connect */
|
/* connect */
|
||||||
const onConnectStart = useCallback(
|
const onConnectStart = useCallback(
|
||||||
(event: any, params: OnConnectStartParams) => {
|
(event: any, params: OnConnectStartParams) => {
|
||||||
if (!params.nodeId) return;
|
const { nodeId, handleId } = params;
|
||||||
|
if (!nodeId) return;
|
||||||
|
|
||||||
// If node is folded, unfold it when connecting
|
// If node is folded, unfold it when connecting
|
||||||
const sourceNode = nodeList.find((node) => node.nodeId === params.nodeId);
|
const sourceNode = nodeList.find((node) => node.nodeId === nodeId);
|
||||||
if (sourceNode?.isFolded) {
|
if (sourceNode?.isFolded) {
|
||||||
return onChangeNode({
|
onChangeNode({
|
||||||
nodeId: params.nodeId,
|
nodeId,
|
||||||
type: 'attr',
|
type: 'attr',
|
||||||
key: 'isFolded',
|
key: 'isFolded',
|
||||||
value: false
|
value: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setConnectingEdge(params);
|
setConnectingEdge(params);
|
||||||
|
|
||||||
|
// Check connect or click(If the mouse position remains basically unchanged, it indicates a click)
|
||||||
|
if (params.handleId) {
|
||||||
|
const initialX = event.clientX;
|
||||||
|
const initialY = event.clientY;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const handleMouseUp = (moveEvent: MouseEvent) => {
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
const currentX = moveEvent.clientX;
|
||||||
|
const currentY = moveEvent.clientY;
|
||||||
|
const endTime = Date.now();
|
||||||
|
const pressDuration = endTime - startTime;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(currentX - initialX) <= 5 &&
|
||||||
|
Math.abs(currentY - initialY) <= 5 &&
|
||||||
|
pressDuration < 500
|
||||||
|
) {
|
||||||
|
const popoverPosition = getTemplatesListPopoverPosition({ nodeId });
|
||||||
|
const addNodePosition = getAddNodePosition({ nodeId, handleId });
|
||||||
|
setHandleParams({
|
||||||
|
...params,
|
||||||
|
popoverPosition,
|
||||||
|
addNodePosition
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[nodeList, setConnectingEdge, onChangeNode]
|
[
|
||||||
|
nodeList,
|
||||||
|
setConnectingEdge,
|
||||||
|
onChangeNode,
|
||||||
|
getTemplatesListPopoverPosition,
|
||||||
|
getAddNodePosition,
|
||||||
|
setHandleParams
|
||||||
|
]
|
||||||
);
|
);
|
||||||
const onConnectEnd = useCallback(() => {
|
const onConnectEnd = useCallback(() => {
|
||||||
setConnectingEdge(undefined);
|
setConnectingEdge(undefined);
|
||||||
@@ -629,7 +728,6 @@ export const useWorkflow = () => {
|
|||||||
},
|
},
|
||||||
[setMenu]
|
[setMenu]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onPaneClick = useCallback(() => {
|
const onPaneClick = useCallback(() => {
|
||||||
setMenu(null);
|
setMenu(null);
|
||||||
}, [setMenu]);
|
}, [setMenu]);
|
||||||
|
@@ -19,6 +19,7 @@ import FlowController from './components/FlowController';
|
|||||||
import ContextMenu from './components/ContextMenu';
|
import ContextMenu from './components/ContextMenu';
|
||||||
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../context/workflowInitContext';
|
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../context/workflowInitContext';
|
||||||
import { WorkflowEventContext } from '../context/workflowEventContext';
|
import { WorkflowEventContext } from '../context/workflowEventContext';
|
||||||
|
import NodeTemplatesPopover from './NodeTemplatesPopover';
|
||||||
|
|
||||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||||
@@ -127,6 +128,7 @@ const Workflow = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
||||||
|
<NodeTemplatesPopover />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
@@ -12,7 +12,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||||
import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||||
import { SourceHandle } from './render/Handle';
|
import { MySourceHandle } from './render/Handle';
|
||||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../context';
|
import { WorkflowContext } from '../../context';
|
||||||
@@ -95,7 +95,7 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SourceHandle
|
<MySourceHandle
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
|
@@ -26,7 +26,7 @@ import { WorkflowContext } from '../../../context';
|
|||||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||||
import MyInput from '@/components/MyInput';
|
import MyInput from '@/components/MyInput';
|
||||||
import { getElseIFLabel, getHandleId } from '@fastgpt/global/core/workflow/utils';
|
import { getElseIFLabel, getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { SourceHandle } from '../render/Handle';
|
import { MySourceHandle } from '../render/Handle';
|
||||||
import { Position, useReactFlow } from 'reactflow';
|
import { Position, useReactFlow } from 'reactflow';
|
||||||
import { getRefData } from '@/web/core/workflow/utils';
|
import { getRefData } from '@/web/core/workflow/utils';
|
||||||
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
|
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
|
||||||
@@ -261,7 +261,7 @@ const ListItem = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</Container>
|
</Container>
|
||||||
{!snapshot.isDragging && (
|
{!snapshot.isDragging && (
|
||||||
<SourceHandle
|
<MySourceHandle
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
handleId={handleId}
|
handleId={handleId}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
|
@@ -10,7 +10,7 @@ import { useContextSelector } from 'use-context-selector';
|
|||||||
import { WorkflowContext } from '../../../context';
|
import { WorkflowContext } from '../../../context';
|
||||||
import Container from '../../components/Container';
|
import Container from '../../components/Container';
|
||||||
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag/index';
|
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag/index';
|
||||||
import { SourceHandle } from '../render/Handle';
|
import { MySourceHandle } from '../render/Handle';
|
||||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
import ListItem from './ListItem';
|
import ListItem from './ListItem';
|
||||||
import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
|
import { IfElseResultEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant';
|
||||||
@@ -95,7 +95,7 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
<Box color={'black'} fontSize={'md'} ml={2}>
|
<Box color={'black'} fontSize={'md'} ml={2}>
|
||||||
{IfElseResultEnum.ELSE}
|
{IfElseResultEnum.ELSE}
|
||||||
</Box>
|
</Box>
|
||||||
<SourceHandle
|
<MySourceHandle
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
handleId={elseHandleId}
|
handleId={elseHandleId}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
|
@@ -11,7 +11,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||||
import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||||
import { SourceHandle } from './render/Handle';
|
import { MySourceHandle } from './render/Handle';
|
||||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../context';
|
import { WorkflowContext } from '../../context';
|
||||||
@@ -207,7 +207,7 @@ const OptionItem = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!snapshot.isDragging && (
|
{!snapshot.isDragging && (
|
||||||
<SourceHandle
|
<MySourceHandle
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Position } from 'reactflow';
|
import { Position } from 'reactflow';
|
||||||
import { SourceHandle, TargetHandle } from '.';
|
import { MySourceHandle, MyTargetHandle } from '.';
|
||||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
@@ -18,7 +18,7 @@ export const ConnectionSourceHandle = ({
|
|||||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||||
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
||||||
|
|
||||||
const { showSourceHandle, RightHandle, LeftHandlee, TopHandlee, BottomHandlee } = useMemo(() => {
|
const { showSourceHandle, RightHandle } = useMemo(() => {
|
||||||
const node = nodeList.find((node) => node.nodeId === nodeId);
|
const node = nodeList.find((node) => node.nodeId === nodeId);
|
||||||
|
|
||||||
/* not node/not connecting node, hidden */
|
/* not node/not connecting node, hidden */
|
||||||
@@ -49,7 +49,7 @@ export const ConnectionSourceHandle = ({
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SourceHandle
|
<MySourceHandle
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
handleId={handleId}
|
handleId={handleId}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
@@ -57,80 +57,14 @@ export const ConnectionSourceHandle = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
const LeftHandlee = (() => {
|
|
||||||
const leftTargetConnected = edges.some(
|
|
||||||
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Left)
|
|
||||||
);
|
|
||||||
if (!node || !node?.sourceHandle?.left || leftTargetConnected) return null;
|
|
||||||
|
|
||||||
const handleId = getHandleId(nodeId, 'source', Position.Left);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SourceHandle
|
|
||||||
nodeId={nodeId}
|
|
||||||
handleId={handleId}
|
|
||||||
position={Position.Left}
|
|
||||||
translate={[-8, 0]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
const TopHandlee = (() => {
|
|
||||||
if (
|
|
||||||
edges.some(
|
|
||||||
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const handleId = getHandleId(nodeId, 'source', Position.Top);
|
|
||||||
const topTargetConnected = edges.some(
|
|
||||||
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Top)
|
|
||||||
);
|
|
||||||
if (!node || !node?.sourceHandle?.top || topTargetConnected) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SourceHandle
|
|
||||||
nodeId={nodeId}
|
|
||||||
handleId={handleId}
|
|
||||||
position={Position.Top}
|
|
||||||
translate={[0, -5]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
const BottomHandlee = (() => {
|
|
||||||
const handleId = getHandleId(nodeId, 'source', Position.Bottom);
|
|
||||||
const targetConnected = edges.some(
|
|
||||||
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Bottom)
|
|
||||||
);
|
|
||||||
if (!node || !node?.sourceHandle?.bottom || targetConnected) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SourceHandle
|
|
||||||
nodeId={nodeId}
|
|
||||||
handleId={handleId}
|
|
||||||
position={Position.Bottom}
|
|
||||||
translate={[0, 5]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showSourceHandle,
|
showSourceHandle,
|
||||||
RightHandle,
|
RightHandle
|
||||||
LeftHandlee,
|
|
||||||
TopHandlee,
|
|
||||||
BottomHandlee
|
|
||||||
};
|
};
|
||||||
}, [connectingEdge, edges, nodeId, nodeList, isFoldNode]);
|
}, [connectingEdge, edges, nodeId, nodeList, isFoldNode]);
|
||||||
|
|
||||||
return showSourceHandle ? (
|
return showSourceHandle ? <>{RightHandle}</> : null;
|
||||||
<>
|
|
||||||
{RightHandle}
|
|
||||||
{LeftHandlee}
|
|
||||||
{TopHandlee}
|
|
||||||
{BottomHandlee}
|
|
||||||
</>
|
|
||||||
) : null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle({
|
export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle({
|
||||||
@@ -141,7 +75,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
|
|||||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||||
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
||||||
|
|
||||||
const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
|
const { LeftHandle } = useMemo(() => {
|
||||||
let node: FlowNodeItemType | undefined = undefined,
|
let node: FlowNodeItemType | undefined = undefined,
|
||||||
connectingNode: FlowNodeItemType | undefined = undefined;
|
connectingNode: FlowNodeItemType | undefined = undefined;
|
||||||
for (const item of nodeList) {
|
for (const item of nodeList) {
|
||||||
@@ -196,7 +130,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
|
|||||||
const handleId = getHandleId(nodeId, 'target', Position.Left);
|
const handleId = getHandleId(nodeId, 'target', Position.Left);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TargetHandle
|
<MyTargetHandle
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
handleId={handleId}
|
handleId={handleId}
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
@@ -205,69 +139,14 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
const rightHandle = (() => {
|
|
||||||
if (!node || !node?.targetHandle?.right) return null;
|
|
||||||
|
|
||||||
const handleId = getHandleId(nodeId, 'target', Position.Right);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TargetHandle
|
|
||||||
nodeId={nodeId}
|
|
||||||
handleId={handleId}
|
|
||||||
position={Position.Right}
|
|
||||||
translate={[4, 0]}
|
|
||||||
showHandle={showHandle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
const topHandle = (() => {
|
|
||||||
if (!node || !node?.targetHandle?.top) return null;
|
|
||||||
|
|
||||||
const handleId = getHandleId(nodeId, 'target', Position.Top);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TargetHandle
|
|
||||||
nodeId={nodeId}
|
|
||||||
handleId={handleId}
|
|
||||||
position={Position.Top}
|
|
||||||
translate={[0, -5]}
|
|
||||||
showHandle={showHandle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
const bottomHandle = (() => {
|
|
||||||
if (!node || !node?.targetHandle?.bottom) return null;
|
|
||||||
|
|
||||||
const handleId = getHandleId(nodeId, 'target', Position.Bottom);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TargetHandle
|
|
||||||
nodeId={nodeId}
|
|
||||||
handleId={handleId}
|
|
||||||
position={Position.Bottom}
|
|
||||||
translate={[0, 5]}
|
|
||||||
showHandle={showHandle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showHandle,
|
showHandle,
|
||||||
LeftHandle,
|
LeftHandle
|
||||||
rightHandle,
|
|
||||||
topHandle,
|
|
||||||
bottomHandle
|
|
||||||
};
|
};
|
||||||
}, [connectingEdge, edges, nodeId, nodeList]);
|
}, [connectingEdge, edges, nodeId, nodeList]);
|
||||||
|
|
||||||
return (
|
return <>{LeftHandle}</>;
|
||||||
<>
|
|
||||||
{LeftHandle}
|
|
||||||
{rightHandle}
|
|
||||||
{topHandle}
|
|
||||||
{bottomHandle}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Dom() {
|
export default function Dom() {
|
||||||
|
@@ -7,26 +7,30 @@ import { useCallback, useMemo } from 'react';
|
|||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
|
import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
|
||||||
import { WorkflowNodeEdgeContext } from '../../../../context/workflowInitContext';
|
import { WorkflowNodeEdgeContext } from '../../../../context/workflowInitContext';
|
||||||
|
import { WorkflowEventContext } from '../../../../context/workflowEventContext';
|
||||||
|
|
||||||
const handleSize = '16px';
|
const handleSize = '16px';
|
||||||
|
const activeHandleSize = '20px';
|
||||||
|
const handleId = NodeOutputKeyEnum.selectedTools;
|
||||||
|
|
||||||
type ToolHandleProps = BoxProps & {
|
type ToolHandleProps = BoxProps & {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
};
|
};
|
||||||
export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
||||||
const { t } = useTranslation();
|
const toolConnecting = useContextSelector(
|
||||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
WorkflowContext,
|
||||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
(ctx) => ctx.connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools
|
||||||
|
);
|
||||||
const handleId = NodeOutputKeyEnum.selectedTools;
|
const connected = useContextSelector(WorkflowNodeEdgeContext, (v) =>
|
||||||
|
v.edges.some((edge) => edge.target === nodeId && edge.targetHandle === handleId)
|
||||||
const connected = edges.some((edge) => edge.target === nodeId && edge.targetHandle === handleId);
|
);
|
||||||
|
|
||||||
|
const active = show && toolConnecting;
|
||||||
// if top handle is connected, return null
|
// if top handle is connected, return null
|
||||||
const showHandle = connectingEdge
|
const showHandle = active || connected;
|
||||||
? show && connectingEdge.handleId === NodeOutputKeyEnum.selectedTools
|
|
||||||
: connected;
|
const size = active ? activeHandleSize : handleSize;
|
||||||
|
|
||||||
const Render = useMemo(() => {
|
const Render = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -35,8 +39,8 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
|||||||
borderRadius: '0',
|
borderRadius: '0',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
width: handleSize,
|
width: size,
|
||||||
height: handleSize,
|
height: size,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -50,8 +54,8 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
|||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="flow-handle"
|
className="flow-handle"
|
||||||
w={handleSize}
|
w={size}
|
||||||
h={handleSize}
|
h={size}
|
||||||
border={'4px solid #8774EE'}
|
border={'4px solid #8774EE'}
|
||||||
rounded={'xs'}
|
rounded={'xs'}
|
||||||
bg={'white'}
|
bg={'white'}
|
||||||
@@ -60,14 +64,21 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
|||||||
/>
|
/>
|
||||||
</Handle>
|
</Handle>
|
||||||
);
|
);
|
||||||
}, [handleId, showHandle]);
|
}, [showHandle, size]);
|
||||||
|
|
||||||
return Render;
|
return Render;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ToolSourceHandle = () => {
|
export const ToolSourceHandle = ({ nodeId }: { nodeId: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
|
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
|
||||||
|
const connectingEdge = useContextSelector(
|
||||||
|
WorkflowContext,
|
||||||
|
(ctx) => ctx.connectingEdge?.nodeId === nodeId
|
||||||
|
);
|
||||||
|
const nodeIsHover = useContextSelector(WorkflowEventContext, (v) => v.hoverNodeId === nodeId);
|
||||||
|
|
||||||
|
const active = useMemo(() => nodeIsHover || connectingEdge, [nodeIsHover, connectingEdge]);
|
||||||
|
|
||||||
/* onConnect edge, delete tool input and switch */
|
/* onConnect edge, delete tool input and switch */
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
@@ -83,6 +94,8 @@ export const ToolSourceHandle = () => {
|
|||||||
[setEdges]
|
[setEdges]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const size = active ? activeHandleSize : handleSize;
|
||||||
|
|
||||||
const Render = useMemo(() => {
|
const Render = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<MyTooltip label={t('common:core.workflow.tool.Handle')} shouldWrapChildren={false}>
|
<MyTooltip label={t('common:core.workflow.tool.Handle')} shouldWrapChildren={false}>
|
||||||
@@ -91,8 +104,8 @@ export const ToolSourceHandle = () => {
|
|||||||
borderRadius: '0',
|
borderRadius: '0',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
width: handleSize,
|
width: size,
|
||||||
height: handleSize,
|
height: size,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -104,8 +117,8 @@ export const ToolSourceHandle = () => {
|
|||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
w={handleSize}
|
w={size}
|
||||||
h={handleSize}
|
h={size}
|
||||||
border={'4px solid #8774EE'}
|
border={'4px solid #8774EE'}
|
||||||
rounded={'xs'}
|
rounded={'xs'}
|
||||||
bg={'white'}
|
bg={'white'}
|
||||||
@@ -115,7 +128,7 @@ export const ToolSourceHandle = () => {
|
|||||||
</Handle>
|
</Handle>
|
||||||
</MyTooltip>
|
</MyTooltip>
|
||||||
);
|
);
|
||||||
}, [onConnect, t]);
|
}, [onConnect, size, t]);
|
||||||
|
|
||||||
return Render;
|
return Render;
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Handle, Position } from 'reactflow';
|
import { Handle, Position } from 'reactflow';
|
||||||
import { handleHighLightStyle, sourceCommonStyle, handleConnectedStyle, handleSize } from './style';
|
|
||||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../../../context';
|
import { WorkflowContext } from '../../../../context';
|
||||||
@@ -10,6 +9,37 @@ import {
|
|||||||
WorkflowInitContext
|
WorkflowInitContext
|
||||||
} from '../../../../context/workflowInitContext';
|
} from '../../../../context/workflowInitContext';
|
||||||
import { WorkflowEventContext } from '../../../../context/workflowEventContext';
|
import { WorkflowEventContext } from '../../../../context/workflowEventContext';
|
||||||
|
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const handleSizeConnected = 16;
|
||||||
|
const handleSizeConnecting = 30;
|
||||||
|
const handleAddIconSize = 22;
|
||||||
|
|
||||||
|
const sourceCommonStyle = {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '50%'
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnectedStyle = {
|
||||||
|
...sourceCommonStyle,
|
||||||
|
borderWidth: '3px',
|
||||||
|
borderColor: '#94B5FF',
|
||||||
|
width: handleSizeConnected,
|
||||||
|
height: handleSizeConnected
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHighLightStyle = {
|
||||||
|
...sourceCommonStyle,
|
||||||
|
borderWidth: '4px',
|
||||||
|
borderColor: '#487FFF',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: handleSizeConnecting,
|
||||||
|
height: handleSizeConnecting
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -18,23 +48,21 @@ type Props = {
|
|||||||
translate?: [number, number];
|
translate?: [number, number];
|
||||||
};
|
};
|
||||||
|
|
||||||
const MySourceHandle = React.memo(function MySourceHandle({
|
export const MySourceHandle = React.memo(function MySourceHandle({
|
||||||
nodeId,
|
nodeId,
|
||||||
translate,
|
translate,
|
||||||
handleId,
|
handleId,
|
||||||
position,
|
position
|
||||||
highlightStyle,
|
}: Props) {
|
||||||
connectedStyle
|
const { t } = useTranslation();
|
||||||
}: Props & {
|
|
||||||
highlightStyle: Record<string, any>;
|
|
||||||
connectedStyle: Record<string, any>;
|
|
||||||
}) {
|
|
||||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||||
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
const node = useContextSelector(WorkflowInitContext, (v) =>
|
||||||
|
v.nodes.find((node) => node.data.nodeId === nodeId)
|
||||||
|
);
|
||||||
const hoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.hoverNodeId);
|
const hoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.hoverNodeId);
|
||||||
|
|
||||||
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
|
|
||||||
const connected = edges.some((edge) => edge.sourceHandle === handleId);
|
const connected = edges.some((edge) => edge.sourceHandle === handleId);
|
||||||
const nodeFolded = node?.data.isFolded && edges.some((edge) => edge.source === nodeId);
|
const nodeFolded = node?.data.isFolded && edges.some((edge) => edge.source === nodeId);
|
||||||
const nodeIsHover = hoverNodeId === nodeId;
|
const nodeIsHover = hoverNodeId === nodeId;
|
||||||
@@ -46,32 +74,16 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
|||||||
const translateStr = useMemo(() => {
|
const translateStr = useMemo(() => {
|
||||||
if (!translate) return '';
|
if (!translate) return '';
|
||||||
if (position === Position.Right) {
|
if (position === Position.Right) {
|
||||||
return `${active ? translate[0] + 2 : translate[0]}px, -50%`;
|
return `${active ? translate[0] + 6 : translate[0]}px, -50%`;
|
||||||
}
|
|
||||||
if (position === Position.Left) {
|
|
||||||
return `${active ? translate[0] + 2 : translate[0]}px, -50%`;
|
|
||||||
}
|
|
||||||
if (position === Position.Top) {
|
|
||||||
return `-50%, ${active ? translate[1] - 2 : translate[1]}px`;
|
|
||||||
}
|
|
||||||
if (position === Position.Bottom) {
|
|
||||||
return `-50%, ${active ? translate[1] + 2 : translate[1]}px`;
|
|
||||||
}
|
}
|
||||||
}, [active, position, translate]);
|
}, [active, position, translate]);
|
||||||
|
|
||||||
const transform = useMemo(
|
|
||||||
() => (translateStr ? `translate(${translateStr})` : ''),
|
|
||||||
[translateStr]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { styles, showAddIcon } = useMemo(() => {
|
const { styles, showAddIcon } = useMemo(() => {
|
||||||
if (active) {
|
if (active) {
|
||||||
return {
|
return {
|
||||||
styles: {
|
styles: {
|
||||||
...highlightStyle,
|
...handleHighLightStyle,
|
||||||
...(translateStr && {
|
transform: `${translateStr ? `translate(${translateStr})` : ''}`
|
||||||
transform
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
showAddIcon: true
|
showAddIcon: true
|
||||||
};
|
};
|
||||||
@@ -80,33 +92,42 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
|||||||
if (connected || nodeFolded) {
|
if (connected || nodeFolded) {
|
||||||
return {
|
return {
|
||||||
styles: {
|
styles: {
|
||||||
...connectedStyle,
|
...handleConnectedStyle,
|
||||||
...(translateStr && {
|
transform: `${translateStr ? `translate(${translateStr})` : ''}`
|
||||||
transform
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
showAddIcon: false
|
showAddIcon: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
styles: undefined,
|
styles: {
|
||||||
|
visibility: 'hidden' as const
|
||||||
|
},
|
||||||
showAddIcon: false
|
showAddIcon: false
|
||||||
};
|
};
|
||||||
}, [active, connected, nodeFolded, highlightStyle, translateStr, transform, connectedStyle]);
|
}, [active, connected, nodeFolded, translateStr]);
|
||||||
|
|
||||||
const RenderHandle = useMemo(() => {
|
if (!node) return null;
|
||||||
return (
|
if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyTooltip
|
||||||
|
label={
|
||||||
|
<Box>
|
||||||
|
<Flex>
|
||||||
|
<Box color={'myGray.900'}>{t('workflow:Click')}</Box>
|
||||||
|
<Box color={'myGray.600'}>{t('workflow:to_add_node')}</Box>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Box color={'myGray.900'}>{t('workflow:Drag')}</Box>
|
||||||
|
<Box color={'myGray.600'}>{t('workflow:to_connect_node')}</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
shouldWrapChildren={false}
|
||||||
|
>
|
||||||
<Handle
|
<Handle
|
||||||
style={
|
style={styles}
|
||||||
!!styles
|
|
||||||
? styles
|
|
||||||
: {
|
|
||||||
visibility: 'hidden',
|
|
||||||
transform,
|
|
||||||
...handleSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type="source"
|
type="source"
|
||||||
id={handleId}
|
id={handleId}
|
||||||
position={position}
|
position={position}
|
||||||
@@ -117,125 +138,73 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
|||||||
name={'edgeAdd'}
|
name={'edgeAdd'}
|
||||||
color={'primary.500'}
|
color={'primary.500'}
|
||||||
pointerEvents={'none'}
|
pointerEvents={'none'}
|
||||||
w={'14px'}
|
w={`${handleAddIconSize}px`}
|
||||||
h={'14px'}
|
h={`${handleAddIconSize}px`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Handle>
|
</Handle>
|
||||||
);
|
</MyTooltip>
|
||||||
}, [handleId, position, showAddIcon, styles, transform]);
|
);
|
||||||
|
|
||||||
if (!node) return null;
|
|
||||||
if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null;
|
|
||||||
|
|
||||||
return <>{RenderHandle}</>;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SourceHandle = (props: Props) => {
|
export const MyTargetHandle = React.memo(function MyTargetHandle({
|
||||||
return (
|
|
||||||
<MySourceHandle
|
|
||||||
{...props}
|
|
||||||
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
|
|
||||||
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MyTargetHandle = React.memo(function MyTargetHandle({
|
|
||||||
nodeId,
|
nodeId,
|
||||||
handleId,
|
handleId,
|
||||||
position,
|
position,
|
||||||
translate,
|
translate,
|
||||||
highlightStyle,
|
|
||||||
connectedStyle,
|
|
||||||
showHandle
|
showHandle
|
||||||
}: Props & {
|
}: Props & {
|
||||||
showHandle: boolean;
|
showHandle: boolean;
|
||||||
highlightStyle: Record<string, any>;
|
|
||||||
connectedStyle: Record<string, any>;
|
|
||||||
}) {
|
}) {
|
||||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
const connected = useContextSelector(WorkflowNodeEdgeContext, (v) =>
|
||||||
|
v.edges.some((edge) => edge.targetHandle === handleId)
|
||||||
|
);
|
||||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||||
|
|
||||||
const connected = edges.some((edge) => edge.targetHandle === handleId);
|
|
||||||
|
|
||||||
const translateStr = useMemo(() => {
|
const translateStr = useMemo(() => {
|
||||||
if (!translate) return '';
|
if (!translate) return '';
|
||||||
|
|
||||||
if (position === Position.Right) {
|
|
||||||
return `${connectingEdge ? translate[0] + 2 : translate[0]}px, -50%`;
|
|
||||||
}
|
|
||||||
if (position === Position.Left) {
|
if (position === Position.Left) {
|
||||||
return `${connectingEdge ? translate[0] - 2 : translate[0]}px, -50%`;
|
return `${connectingEdge ? translate[0] - 6 : translate[0]}px, -50%`;
|
||||||
}
|
|
||||||
if (position === Position.Top) {
|
|
||||||
return `-50%, ${connectingEdge ? translate[1] - 2 : translate[1]}px`;
|
|
||||||
}
|
|
||||||
if (position === Position.Bottom) {
|
|
||||||
return `-50%, ${connectingEdge ? translate[1] + 2 : translate[1]}px`;
|
|
||||||
}
|
}
|
||||||
}, [connectingEdge, position, translate]);
|
}, [connectingEdge, position, translate]);
|
||||||
|
|
||||||
const transform = useMemo(
|
|
||||||
() => (translateStr ? `translate(${translateStr})` : ''),
|
|
||||||
[translateStr]
|
|
||||||
);
|
|
||||||
|
|
||||||
const styles = useMemo(() => {
|
const styles = useMemo(() => {
|
||||||
if (!connectingEdge && !connected) return;
|
if ((!connectingEdge && !connected) || !showHandle) {
|
||||||
|
return {
|
||||||
|
visibility: 'hidden' as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (connectingEdge) {
|
if (connectingEdge) {
|
||||||
return {
|
return {
|
||||||
...highlightStyle,
|
...handleHighLightStyle,
|
||||||
transform
|
transform: `${translateStr ? `translate(${translateStr})` : ''}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
return {
|
return {
|
||||||
...connectedStyle,
|
...handleConnectedStyle,
|
||||||
transform
|
transform: `${translateStr ? `translate(${translateStr})` : ''}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return;
|
return {
|
||||||
}, [connected, connectingEdge, connectedStyle, highlightStyle, transform]);
|
visibility: 'hidden' as const
|
||||||
|
};
|
||||||
|
}, [connected, connectingEdge, showHandle, translateStr]);
|
||||||
|
|
||||||
const RenderHandle = useMemo(() => {
|
|
||||||
return (
|
|
||||||
<Handle
|
|
||||||
style={
|
|
||||||
styles && showHandle
|
|
||||||
? styles
|
|
||||||
: {
|
|
||||||
visibility: 'hidden',
|
|
||||||
transform,
|
|
||||||
...handleSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isConnectableEnd={styles && showHandle}
|
|
||||||
type="target"
|
|
||||||
id={handleId}
|
|
||||||
position={position}
|
|
||||||
></Handle>
|
|
||||||
);
|
|
||||||
}, [styles, showHandle, transform, handleId, position]);
|
|
||||||
|
|
||||||
return RenderHandle;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const TargetHandle = (
|
|
||||||
props: Props & {
|
|
||||||
showHandle: boolean;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
return (
|
return (
|
||||||
<MyTargetHandle
|
<Handle
|
||||||
{...props}
|
style={styles}
|
||||||
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
|
isConnectableEnd={styles && showHandle}
|
||||||
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
|
isConnectableStart={false}
|
||||||
|
type="target"
|
||||||
|
id={handleId}
|
||||||
|
position={position}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default function Dom() {
|
export default function Dom() {
|
||||||
return <></>;
|
return <></>;
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
export const primaryColor = '#487FFF';
|
|
||||||
export const lowPrimaryColor = '#94B5FF';
|
|
||||||
export const handleSize = {
|
|
||||||
width: '20px',
|
|
||||||
height: '20px'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sourceCommonStyle = {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderWidth: '3px',
|
|
||||||
borderRadius: '50%'
|
|
||||||
};
|
|
||||||
export const handleConnectedStyle = {
|
|
||||||
borderColor: lowPrimaryColor,
|
|
||||||
width: '16px',
|
|
||||||
height: '16px'
|
|
||||||
};
|
|
||||||
export const handleHighLightStyle = {
|
|
||||||
borderColor: primaryColor,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '20px',
|
|
||||||
height: '20px'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Dom() {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
@@ -308,8 +308,9 @@ const NodeCard = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}, [nodeId, isFolded]);
|
}, [nodeId, isFolded]);
|
||||||
const RenderToolHandle = useMemo(
|
const RenderToolHandle = useMemo(
|
||||||
() => (node?.flowNodeType === FlowNodeTypeEnum.tools ? <ToolSourceHandle /> : null),
|
() =>
|
||||||
[node?.flowNodeType]
|
node?.flowNodeType === FlowNodeTypeEnum.tools ? <ToolSourceHandle nodeId={nodeId} /> : null,
|
||||||
|
[node?.flowNodeType, nodeId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
import { SourceHandle } from '../Handle';
|
import { MySourceHandle } from '../Handle';
|
||||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { Position } from 'reactflow';
|
import { Position } from 'reactflow';
|
||||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||||
@@ -74,7 +74,7 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{output.type === FlowNodeOutputTypeEnum.source && (
|
{output.type === FlowNodeOutputTypeEnum.source && (
|
||||||
<SourceHandle
|
<MySourceHandle
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
handleId={getHandleId(nodeId, 'source', output.key)}
|
handleId={getHandleId(nodeId, 'source', output.key)}
|
||||||
translate={[34, 0]}
|
translate={[34, 0]}
|
||||||
|
@@ -2,7 +2,7 @@ import { postWorkflowDebug } from '@/web/core/workflow/api';
|
|||||||
import {
|
import {
|
||||||
checkWorkflowNodeAndConnection,
|
checkWorkflowNodeAndConnection,
|
||||||
compareSnapshot,
|
compareSnapshot,
|
||||||
storeEdgesRenderEdge,
|
storeEdge2RenderEdge,
|
||||||
storeNode2FlowNode
|
storeNode2FlowNode
|
||||||
} from '@/web/core/workflow/utils';
|
} from '@/web/core/workflow/utils';
|
||||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||||
@@ -861,7 +861,7 @@ const WorkflowContextProvider = ({
|
|||||||
});
|
});
|
||||||
const onSwitchCloudVersion = useMemoizedFn((appVersion: AppVersionSchemaType) => {
|
const onSwitchCloudVersion = useMemoizedFn((appVersion: AppVersionSchemaType) => {
|
||||||
const nodes = appVersion.nodes.map((item) => storeNode2FlowNode({ item, t }));
|
const nodes = appVersion.nodes.map((item) => storeNode2FlowNode({ item, t }));
|
||||||
const edges = appVersion.edges.map((item) => storeEdgesRenderEdge({ edge: item }));
|
const edges = appVersion.edges.map((item) => storeEdge2RenderEdge({ edge: item }));
|
||||||
const chatConfig = appVersion.chatConfig;
|
const chatConfig = appVersion.chatConfig;
|
||||||
|
|
||||||
resetSnapshot({
|
resetSnapshot({
|
||||||
@@ -912,7 +912,7 @@ const WorkflowContextProvider = ({
|
|||||||
isInit?: boolean
|
isInit?: boolean
|
||||||
) => {
|
) => {
|
||||||
const nodes = e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [];
|
const nodes = e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [];
|
||||||
const edges = e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [];
|
const edges = e.edges?.map((item) => storeEdge2RenderEdge({ edge: item })) || [];
|
||||||
|
|
||||||
// Get storage snapshot,兼容旧版正在编辑的用户,刷新后会把 local 数据存到内存并删除
|
// Get storage snapshot,兼容旧版正在编辑的用户,刷新后会把 local 数据存到内存并删除
|
||||||
const pastSnapshot = (() => {
|
const pastSnapshot = (() => {
|
||||||
|
@@ -2,6 +2,12 @@ import React, { type ReactNode, useEffect, useMemo, useRef, useState } from 'rea
|
|||||||
import { createContext } from 'use-context-selector';
|
import { createContext } from 'use-context-selector';
|
||||||
import { useLocalStorageState } from 'ahooks';
|
import { useLocalStorageState } from 'ahooks';
|
||||||
import { type SetState } from 'ahooks/lib/createUseStorageState';
|
import { type SetState } from 'ahooks/lib/createUseStorageState';
|
||||||
|
import type { OnConnectStartParams } from 'reactflow';
|
||||||
|
|
||||||
|
type handleParamsType = OnConnectStartParams & {
|
||||||
|
popoverPosition: { x: number; y: number };
|
||||||
|
addNodePosition: { x: number; y: number };
|
||||||
|
};
|
||||||
|
|
||||||
type WorkflowEventContextType = {
|
type WorkflowEventContextType = {
|
||||||
mouseInCanvas: boolean;
|
mouseInCanvas: boolean;
|
||||||
@@ -20,6 +26,8 @@ type WorkflowEventContextType = {
|
|||||||
// version history
|
// version history
|
||||||
showHistoryModal: boolean;
|
showHistoryModal: boolean;
|
||||||
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
handleParams: handleParamsType | null;
|
||||||
|
setHandleParams: React.Dispatch<React.SetStateAction<handleParamsType | null>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowEventContext = createContext<WorkflowEventContextType>({
|
export const WorkflowEventContext = createContext<WorkflowEventContextType>({
|
||||||
@@ -42,6 +50,10 @@ export const WorkflowEventContext = createContext<WorkflowEventContextType>({
|
|||||||
showHistoryModal: false,
|
showHistoryModal: false,
|
||||||
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
|
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
handleParams: null,
|
||||||
|
setHandleParams: function (value: React.SetStateAction<handleParamsType | null>): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,6 +61,8 @@ const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) =>
|
|||||||
// Watch mouse in canvas
|
// Watch mouse in canvas
|
||||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||||
const [mouseInCanvas, setMouseInCanvas] = useState(false);
|
const [mouseInCanvas, setMouseInCanvas] = useState(false);
|
||||||
|
const [handleParams, setHandleParams] = useState<handleParamsType | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseInCanvas = (e: MouseEvent) => {
|
const handleMouseInCanvas = (e: MouseEvent) => {
|
||||||
setMouseInCanvas(true);
|
setMouseInCanvas(true);
|
||||||
@@ -62,7 +76,7 @@ const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) =>
|
|||||||
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
|
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
|
||||||
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
|
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
|
||||||
};
|
};
|
||||||
}, [reactFlowWrapper?.current, setMouseInCanvas]);
|
}, [setMouseInCanvas]);
|
||||||
|
|
||||||
// Watch hover node
|
// Watch hover node
|
||||||
const [hoverNodeId, setHoverNodeId] = useState<string>();
|
const [hoverNodeId, setHoverNodeId] = useState<string>();
|
||||||
@@ -95,7 +109,9 @@ const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) =>
|
|||||||
menu,
|
menu,
|
||||||
setMenu,
|
setMenu,
|
||||||
showHistoryModal,
|
showHistoryModal,
|
||||||
setShowHistoryModal
|
setShowHistoryModal,
|
||||||
|
handleParams,
|
||||||
|
setHandleParams
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
mouseInCanvas,
|
mouseInCanvas,
|
||||||
@@ -108,7 +124,9 @@ const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) =>
|
|||||||
menu,
|
menu,
|
||||||
setMenu,
|
setMenu,
|
||||||
showHistoryModal,
|
showHistoryModal,
|
||||||
setShowHistoryModal
|
setShowHistoryModal,
|
||||||
|
handleParams,
|
||||||
|
setHandleParams
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@@ -2,7 +2,7 @@ import { computedNodeInputReference } from '@/web/core/workflow/utils';
|
|||||||
import { type AppDetailType } from '@fastgpt/global/core/app/type';
|
import { type AppDetailType } from '@fastgpt/global/core/app/type';
|
||||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
import { type StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||||
import {
|
import {
|
||||||
type FlowNodeItemType,
|
type FlowNodeItemType,
|
||||||
type StoreNodeItemType
|
type StoreNodeItemType
|
||||||
|
@@ -170,11 +170,16 @@ export const storeNode2FlowNode = ({
|
|||||||
zIndex
|
zIndex
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export const storeEdgesRenderEdge = ({ edge }: { edge: StoreEdgeItemType }) => {
|
export const storeEdge2RenderEdge = ({ edge }: { edge: StoreEdgeItemType }) => {
|
||||||
|
const sourceHandle = edge.sourceHandle.replace(/-source-(top|bottom|left)$/, '-source-right');
|
||||||
|
const targetHandle = edge.targetHandle.replace(/-target-(top|bottom|right)$/, '-target-left');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...edge,
|
...edge,
|
||||||
id: getNanoid(),
|
id: getNanoid(),
|
||||||
type: EDGE_TYPE
|
type: EDGE_TYPE,
|
||||||
|
sourceHandle,
|
||||||
|
targetHandle
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user