mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +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:
@@ -32,7 +32,7 @@ import { isProduction } from '@fastgpt/global/common/system/constants';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import {
|
||||
checkWorkflowNodeAndConnection,
|
||||
storeEdgesRenderEdge,
|
||||
storeEdge2RenderEdge,
|
||||
storeNode2FlowNode
|
||||
} from '@/web/core/workflow/utils';
|
||||
|
||||
@@ -246,7 +246,7 @@ const Header = ({
|
||||
const { nodes: storeNodes, edges: storeEdges } = form2AppWorkflow(appForm, 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 });
|
||||
|
||||
|
@@ -1,212 +1,46 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
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 React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import { type Node } from 'reactflow';
|
||||
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 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 CostTooltip from '@/components/core/app/plugin/CostTooltip';
|
||||
import MyAvatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import NodeTemplateListHeader from './components/NodeTemplates/header';
|
||||
import NodeTemplateList from './components/NodeTemplates/list';
|
||||
import { useNodeTemplates } from './components/NodeTemplates/useNodeTemplates';
|
||||
|
||||
type ModuleTemplateListProps = {
|
||||
isOpen: boolean;
|
||||
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 {
|
||||
'basic' = 'basic',
|
||||
'systemPlugin' = 'systemPlugin',
|
||||
'teamPlugin' = 'teamPlugin'
|
||||
}
|
||||
|
||||
const sliderWidth = 460;
|
||||
export const sliderWidth = 460;
|
||||
|
||||
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
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 setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
||||
|
||||
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 {
|
||||
data: teamAndSystemApps,
|
||||
loading: isLoading,
|
||||
runAsync: loadNodeTemplates
|
||||
} = useRequest2(
|
||||
async ({
|
||||
parentId = '',
|
||||
type = templateType,
|
||||
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]
|
||||
}
|
||||
);
|
||||
templateType,
|
||||
parentId,
|
||||
templatesIsLoading,
|
||||
templates,
|
||||
loadNodeTemplates,
|
||||
onUpdateParentId
|
||||
} = useNodeTemplates();
|
||||
|
||||
const templates = useMemo(
|
||||
() => basicNodes || teamAndSystemApps || [],
|
||||
[basicNodes, teamAndSystemApps]
|
||||
);
|
||||
|
||||
const onUpdateParentId = useCallback(
|
||||
(parentId: ParentIdType) => {
|
||||
loadNodeTemplates({
|
||||
parentId
|
||||
});
|
||||
},
|
||||
[loadNodeTemplates]
|
||||
);
|
||||
|
||||
// Init load refresh templates
|
||||
useRequest2(
|
||||
() =>
|
||||
loadNodeTemplates({
|
||||
parentId: '',
|
||||
searchVal: searchKey
|
||||
}),
|
||||
{
|
||||
manual: false,
|
||||
throttleWait: 300,
|
||||
refreshDeps: [searchKey]
|
||||
}
|
||||
);
|
||||
const onAddNode = useMemoizedFn(async ({ newNodes }: { newNodes: Node<FlowNodeItemType>[] }) => {
|
||||
setNodes((state) => {
|
||||
const newState = state
|
||||
.map((node) => ({
|
||||
...node,
|
||||
selected: false
|
||||
}))
|
||||
// @ts-ignore
|
||||
.concat(newNodes);
|
||||
return newState;
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -223,7 +57,7 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
<MyBox
|
||||
isLoading={isLoading}
|
||||
isLoading={templatesIsLoading}
|
||||
display={'flex'}
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
@@ -241,22 +75,18 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
userSelect={'none'}
|
||||
overflow={isOpen ? 'none' : 'hidden'}
|
||||
>
|
||||
<RenderHeader
|
||||
templateType={templateType}
|
||||
<NodeTemplateListHeader
|
||||
onClose={onClose}
|
||||
parentId={parentId}
|
||||
onUpdateParentId={onUpdateParentId}
|
||||
searchKey={searchKey}
|
||||
templateType={templateType}
|
||||
loadNodeTemplates={loadNodeTemplates}
|
||||
setSearchKey={setSearchKey}
|
||||
parentId={parentId || ''}
|
||||
onUpdateParentId={onUpdateParentId}
|
||||
/>
|
||||
<RenderList
|
||||
templateType={templateType}
|
||||
<NodeTemplateList
|
||||
onAddNode={onAddNode}
|
||||
templates={templates}
|
||||
type={templateType}
|
||||
onClose={onClose}
|
||||
parentId={parentId}
|
||||
setParentId={onUpdateParentId}
|
||||
templateType={templateType}
|
||||
onUpdateParentId={onUpdateParentId}
|
||||
/>
|
||||
</MyBox>
|
||||
</>
|
||||
@@ -264,531 +94,3 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
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 {
|
||||
newTargetX: targetX,
|
||||
newTargetY: targetY
|
||||
@@ -150,6 +132,7 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
return `translate(-50%, -90%) translate(${newTargetX}px,${newTargetY}px) rotate(90deg)`;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<EdgeLabelRenderer>
|
||||
<Box hidden={parentNode?.isFolded}>
|
||||
@@ -160,8 +143,8 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
position={'absolute'}
|
||||
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
|
||||
pointerEvents={'all'}
|
||||
w={'18px'}
|
||||
h={'18px'}
|
||||
w={'26px'}
|
||||
h={'26px'}
|
||||
bg={'white'}
|
||||
borderRadius={'18px'}
|
||||
cursor={'pointer'}
|
||||
@@ -177,8 +160,8 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
position={'absolute'}
|
||||
transform={arrowTransform}
|
||||
pointerEvents={'all'}
|
||||
w={highlightEdge ? '12px' : '10px'}
|
||||
h={highlightEdge ? '12px' : '10px'}
|
||||
w={highlightEdge ? '14px' : '12px'}
|
||||
h={highlightEdge ? '14px' : '12px'}
|
||||
zIndex={highlightEdge ? defaultZIndex + 1000 : defaultZIndex}
|
||||
>
|
||||
<MyIcon
|
||||
@@ -197,8 +180,8 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
highlightEdge,
|
||||
labelX,
|
||||
labelY,
|
||||
isToolEdge,
|
||||
defaultZIndex,
|
||||
isToolEdge,
|
||||
edgeColor,
|
||||
targetPosition,
|
||||
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 = () => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
@@ -289,8 +292,9 @@ export const useWorkflow = () => {
|
||||
WorkflowStatusContext,
|
||||
(v) => v.resetParentNodeSizeAndPosition
|
||||
);
|
||||
const setHandleParams = useContextSelector(WorkflowEventContext, (v) => v.setHandleParams);
|
||||
|
||||
const { getIntersectingNodes } = useReactFlow();
|
||||
const { getIntersectingNodes, flowToScreenPosition, getZoom } = useReactFlow();
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
|
||||
/* 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 */
|
||||
// Remove change node and its child nodes and edges
|
||||
const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, nodeId: string) => {
|
||||
@@ -525,21 +584,61 @@ export const useWorkflow = () => {
|
||||
/* connect */
|
||||
const onConnectStart = useCallback(
|
||||
(event: any, params: OnConnectStartParams) => {
|
||||
if (!params.nodeId) return;
|
||||
const { nodeId, handleId } = params;
|
||||
if (!nodeId) return;
|
||||
|
||||
// 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) {
|
||||
return onChangeNode({
|
||||
nodeId: params.nodeId,
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'attr',
|
||||
key: 'isFolded',
|
||||
value: false
|
||||
});
|
||||
}
|
||||
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(() => {
|
||||
setConnectingEdge(undefined);
|
||||
@@ -629,7 +728,6 @@ export const useWorkflow = () => {
|
||||
},
|
||||
[setMenu]
|
||||
);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
setMenu(null);
|
||||
}, [setMenu]);
|
||||
|
@@ -19,6 +19,7 @@ import FlowController from './components/FlowController';
|
||||
import ContextMenu from './components/ContextMenu';
|
||||
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../context/workflowEventContext';
|
||||
import NodeTemplatesPopover from './NodeTemplatesPopover';
|
||||
|
||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
@@ -127,6 +128,7 @@ const Workflow = () => {
|
||||
}}
|
||||
/>
|
||||
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
||||
<NodeTemplatesPopover />
|
||||
</>
|
||||
|
||||
<ReactFlow
|
||||
|
@@ -12,7 +12,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
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 { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
@@ -95,7 +95,7 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SourceHandle
|
||||
<MySourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||
position={Position.Right}
|
||||
|
@@ -26,7 +26,7 @@ import { WorkflowContext } from '../../../context';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MyInput from '@/components/MyInput';
|
||||
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 { getRefData } from '@/web/core/workflow/utils';
|
||||
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
|
||||
@@ -261,7 +261,7 @@ const ListItem = ({
|
||||
</Button>
|
||||
</Container>
|
||||
{!snapshot.isDragging && (
|
||||
<SourceHandle
|
||||
<MySourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
position={Position.Right}
|
||||
|
@@ -10,7 +10,7 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import Container from '../../components/Container';
|
||||
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 ListItem from './ListItem';
|
||||
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}>
|
||||
{IfElseResultEnum.ELSE}
|
||||
</Box>
|
||||
<SourceHandle
|
||||
<MySourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={elseHandleId}
|
||||
position={Position.Right}
|
||||
|
@@ -11,7 +11,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
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 { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
@@ -207,7 +207,7 @@ const OptionItem = ({
|
||||
}}
|
||||
/>
|
||||
{!snapshot.isDragging && (
|
||||
<SourceHandle
|
||||
<MySourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', item.key)}
|
||||
position={Position.Right}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Position } from 'reactflow';
|
||||
import { SourceHandle, TargetHandle } from '.';
|
||||
import { MySourceHandle, MyTargetHandle } from '.';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
@@ -18,7 +18,7 @@ export const ConnectionSourceHandle = ({
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
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);
|
||||
|
||||
/* not node/not connecting node, hidden */
|
||||
@@ -49,7 +49,7 @@ export const ConnectionSourceHandle = ({
|
||||
return null;
|
||||
|
||||
return (
|
||||
<SourceHandle
|
||||
<MySourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
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 {
|
||||
showSourceHandle,
|
||||
RightHandle,
|
||||
LeftHandlee,
|
||||
TopHandlee,
|
||||
BottomHandlee
|
||||
RightHandle
|
||||
};
|
||||
}, [connectingEdge, edges, nodeId, nodeList, isFoldNode]);
|
||||
|
||||
return showSourceHandle ? (
|
||||
<>
|
||||
{RightHandle}
|
||||
{LeftHandlee}
|
||||
{TopHandlee}
|
||||
{BottomHandlee}
|
||||
</>
|
||||
) : null;
|
||||
return showSourceHandle ? <>{RightHandle}</> : null;
|
||||
};
|
||||
|
||||
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 { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
||||
|
||||
const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
|
||||
const { LeftHandle } = useMemo(() => {
|
||||
let node: FlowNodeItemType | undefined = undefined,
|
||||
connectingNode: FlowNodeItemType | undefined = undefined;
|
||||
for (const item of nodeList) {
|
||||
@@ -196,7 +130,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
|
||||
const handleId = getHandleId(nodeId, 'target', Position.Left);
|
||||
|
||||
return (
|
||||
<TargetHandle
|
||||
<MyTargetHandle
|
||||
nodeId={nodeId}
|
||||
handleId={handleId}
|
||||
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 {
|
||||
showHandle,
|
||||
LeftHandle,
|
||||
rightHandle,
|
||||
topHandle,
|
||||
bottomHandle
|
||||
LeftHandle
|
||||
};
|
||||
}, [connectingEdge, edges, nodeId, nodeList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{LeftHandle}
|
||||
{rightHandle}
|
||||
{topHandle}
|
||||
{bottomHandle}
|
||||
</>
|
||||
);
|
||||
return <>{LeftHandle}</>;
|
||||
});
|
||||
|
||||
export default function Dom() {
|
||||
|
@@ -7,26 +7,30 @@ import { useCallback, useMemo } from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
|
||||
import { WorkflowNodeEdgeContext } from '../../../../context/workflowInitContext';
|
||||
import { WorkflowEventContext } from '../../../../context/workflowEventContext';
|
||||
|
||||
const handleSize = '16px';
|
||||
const activeHandleSize = '20px';
|
||||
const handleId = NodeOutputKeyEnum.selectedTools;
|
||||
|
||||
type ToolHandleProps = BoxProps & {
|
||||
nodeId: string;
|
||||
show: boolean;
|
||||
};
|
||||
export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
|
||||
const handleId = NodeOutputKeyEnum.selectedTools;
|
||||
|
||||
const connected = edges.some((edge) => edge.target === nodeId && edge.targetHandle === handleId);
|
||||
const toolConnecting = useContextSelector(
|
||||
WorkflowContext,
|
||||
(ctx) => ctx.connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools
|
||||
);
|
||||
const connected = useContextSelector(WorkflowNodeEdgeContext, (v) =>
|
||||
v.edges.some((edge) => edge.target === nodeId && edge.targetHandle === handleId)
|
||||
);
|
||||
|
||||
const active = show && toolConnecting;
|
||||
// if top handle is connected, return null
|
||||
const showHandle = connectingEdge
|
||||
? show && connectingEdge.handleId === NodeOutputKeyEnum.selectedTools
|
||||
: connected;
|
||||
const showHandle = active || connected;
|
||||
|
||||
const size = active ? activeHandleSize : handleSize;
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
@@ -35,8 +39,8 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
||||
borderRadius: '0',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
width: handleSize,
|
||||
height: handleSize,
|
||||
width: size,
|
||||
height: size,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -50,8 +54,8 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
||||
>
|
||||
<Box
|
||||
className="flow-handle"
|
||||
w={handleSize}
|
||||
h={handleSize}
|
||||
w={size}
|
||||
h={size}
|
||||
border={'4px solid #8774EE'}
|
||||
rounded={'xs'}
|
||||
bg={'white'}
|
||||
@@ -60,14 +64,21 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
||||
/>
|
||||
</Handle>
|
||||
);
|
||||
}, [handleId, showHandle]);
|
||||
}, [showHandle, size]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export const ToolSourceHandle = () => {
|
||||
export const ToolSourceHandle = ({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
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 */
|
||||
const onConnect = useCallback(
|
||||
@@ -83,6 +94,8 @@ export const ToolSourceHandle = () => {
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const size = active ? activeHandleSize : handleSize;
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<MyTooltip label={t('common:core.workflow.tool.Handle')} shouldWrapChildren={false}>
|
||||
@@ -91,8 +104,8 @@ export const ToolSourceHandle = () => {
|
||||
borderRadius: '0',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
width: handleSize,
|
||||
height: handleSize,
|
||||
width: size,
|
||||
height: size,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -104,8 +117,8 @@ export const ToolSourceHandle = () => {
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<Box
|
||||
w={handleSize}
|
||||
h={handleSize}
|
||||
w={size}
|
||||
h={size}
|
||||
border={'4px solid #8774EE'}
|
||||
rounded={'xs'}
|
||||
bg={'white'}
|
||||
@@ -115,7 +128,7 @@ export const ToolSourceHandle = () => {
|
||||
</Handle>
|
||||
</MyTooltip>
|
||||
);
|
||||
}, [onConnect, t]);
|
||||
}, [onConnect, size, t]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import { handleHighLightStyle, sourceCommonStyle, handleConnectedStyle, handleSize } from './style';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../../context';
|
||||
@@ -10,6 +9,37 @@ import {
|
||||
WorkflowInitContext
|
||||
} from '../../../../context/workflowInitContext';
|
||||
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 = {
|
||||
nodeId: string;
|
||||
@@ -18,23 +48,21 @@ type Props = {
|
||||
translate?: [number, number];
|
||||
};
|
||||
|
||||
const MySourceHandle = React.memo(function MySourceHandle({
|
||||
export const MySourceHandle = React.memo(function MySourceHandle({
|
||||
nodeId,
|
||||
translate,
|
||||
handleId,
|
||||
position,
|
||||
highlightStyle,
|
||||
connectedStyle
|
||||
}: Props & {
|
||||
highlightStyle: Record<string, any>;
|
||||
connectedStyle: Record<string, any>;
|
||||
}) {
|
||||
position
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
|
||||
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 node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
|
||||
const connected = edges.some((edge) => edge.sourceHandle === handleId);
|
||||
const nodeFolded = node?.data.isFolded && edges.some((edge) => edge.source === nodeId);
|
||||
const nodeIsHover = hoverNodeId === nodeId;
|
||||
@@ -46,32 +74,16 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
||||
const translateStr = useMemo(() => {
|
||||
if (!translate) return '';
|
||||
if (position === Position.Right) {
|
||||
return `${active ? translate[0] + 2 : 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`;
|
||||
return `${active ? translate[0] + 6 : translate[0]}px, -50%`;
|
||||
}
|
||||
}, [active, position, translate]);
|
||||
|
||||
const transform = useMemo(
|
||||
() => (translateStr ? `translate(${translateStr})` : ''),
|
||||
[translateStr]
|
||||
);
|
||||
|
||||
const { styles, showAddIcon } = useMemo(() => {
|
||||
if (active) {
|
||||
return {
|
||||
styles: {
|
||||
...highlightStyle,
|
||||
...(translateStr && {
|
||||
transform
|
||||
})
|
||||
...handleHighLightStyle,
|
||||
transform: `${translateStr ? `translate(${translateStr})` : ''}`
|
||||
},
|
||||
showAddIcon: true
|
||||
};
|
||||
@@ -80,33 +92,42 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
||||
if (connected || nodeFolded) {
|
||||
return {
|
||||
styles: {
|
||||
...connectedStyle,
|
||||
...(translateStr && {
|
||||
transform
|
||||
})
|
||||
...handleConnectedStyle,
|
||||
transform: `${translateStr ? `translate(${translateStr})` : ''}`
|
||||
},
|
||||
showAddIcon: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
styles: undefined,
|
||||
styles: {
|
||||
visibility: 'hidden' as const
|
||||
},
|
||||
showAddIcon: false
|
||||
};
|
||||
}, [active, connected, nodeFolded, highlightStyle, translateStr, transform, connectedStyle]);
|
||||
}, [active, connected, nodeFolded, translateStr]);
|
||||
|
||||
const RenderHandle = useMemo(() => {
|
||||
return (
|
||||
if (!node) return null;
|
||||
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
|
||||
style={
|
||||
!!styles
|
||||
? styles
|
||||
: {
|
||||
visibility: 'hidden',
|
||||
transform,
|
||||
...handleSize
|
||||
}
|
||||
}
|
||||
style={styles}
|
||||
type="source"
|
||||
id={handleId}
|
||||
position={position}
|
||||
@@ -117,125 +138,73 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
||||
name={'edgeAdd'}
|
||||
color={'primary.500'}
|
||||
pointerEvents={'none'}
|
||||
w={'14px'}
|
||||
h={'14px'}
|
||||
w={`${handleAddIconSize}px`}
|
||||
h={`${handleAddIconSize}px`}
|
||||
/>
|
||||
)}
|
||||
</Handle>
|
||||
);
|
||||
}, [handleId, position, showAddIcon, styles, transform]);
|
||||
|
||||
if (!node) return null;
|
||||
if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null;
|
||||
|
||||
return <>{RenderHandle}</>;
|
||||
</MyTooltip>
|
||||
);
|
||||
});
|
||||
|
||||
export const SourceHandle = (props: Props) => {
|
||||
return (
|
||||
<MySourceHandle
|
||||
{...props}
|
||||
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
|
||||
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MyTargetHandle = React.memo(function MyTargetHandle({
|
||||
export const MyTargetHandle = React.memo(function MyTargetHandle({
|
||||
nodeId,
|
||||
handleId,
|
||||
position,
|
||||
translate,
|
||||
highlightStyle,
|
||||
connectedStyle,
|
||||
showHandle
|
||||
}: Props & {
|
||||
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 connected = edges.some((edge) => edge.targetHandle === handleId);
|
||||
|
||||
const translateStr = useMemo(() => {
|
||||
if (!translate) return '';
|
||||
|
||||
if (position === Position.Right) {
|
||||
return `${connectingEdge ? translate[0] + 2 : translate[0]}px, -50%`;
|
||||
}
|
||||
if (position === Position.Left) {
|
||||
return `${connectingEdge ? translate[0] - 2 : 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`;
|
||||
return `${connectingEdge ? translate[0] - 6 : translate[0]}px, -50%`;
|
||||
}
|
||||
}, [connectingEdge, position, translate]);
|
||||
|
||||
const transform = useMemo(
|
||||
() => (translateStr ? `translate(${translateStr})` : ''),
|
||||
[translateStr]
|
||||
);
|
||||
|
||||
const styles = useMemo(() => {
|
||||
if (!connectingEdge && !connected) return;
|
||||
if ((!connectingEdge && !connected) || !showHandle) {
|
||||
return {
|
||||
visibility: 'hidden' as const
|
||||
};
|
||||
}
|
||||
|
||||
if (connectingEdge) {
|
||||
return {
|
||||
...highlightStyle,
|
||||
transform
|
||||
...handleHighLightStyle,
|
||||
transform: `${translateStr ? `translate(${translateStr})` : ''}`
|
||||
};
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
return {
|
||||
...connectedStyle,
|
||||
transform
|
||||
...handleConnectedStyle,
|
||||
transform: `${translateStr ? `translate(${translateStr})` : ''}`
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [connected, connectingEdge, connectedStyle, highlightStyle, transform]);
|
||||
return {
|
||||
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 (
|
||||
<MyTargetHandle
|
||||
{...props}
|
||||
highlightStyle={{ ...sourceCommonStyle, ...handleHighLightStyle }}
|
||||
connectedStyle={{ ...sourceCommonStyle, ...handleConnectedStyle }}
|
||||
<Handle
|
||||
style={styles}
|
||||
isConnectableEnd={styles && showHandle}
|
||||
isConnectableStart={false}
|
||||
type="target"
|
||||
id={handleId}
|
||||
position={position}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default function Dom() {
|
||||
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]);
|
||||
const RenderToolHandle = useMemo(
|
||||
() => (node?.flowNodeType === FlowNodeTypeEnum.tools ? <ToolSourceHandle /> : null),
|
||||
[node?.flowNodeType]
|
||||
() =>
|
||||
node?.flowNodeType === FlowNodeTypeEnum.tools ? <ToolSourceHandle nodeId={nodeId} /> : null,
|
||||
[node?.flowNodeType, nodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
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 { Position } from 'reactflow';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
@@ -74,7 +74,7 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
|
||||
)}
|
||||
</Flex>
|
||||
{output.type === FlowNodeOutputTypeEnum.source && (
|
||||
<SourceHandle
|
||||
<MySourceHandle
|
||||
nodeId={nodeId}
|
||||
handleId={getHandleId(nodeId, 'source', output.key)}
|
||||
translate={[34, 0]}
|
||||
|
@@ -2,7 +2,7 @@ import { postWorkflowDebug } from '@/web/core/workflow/api';
|
||||
import {
|
||||
checkWorkflowNodeAndConnection,
|
||||
compareSnapshot,
|
||||
storeEdgesRenderEdge,
|
||||
storeEdge2RenderEdge,
|
||||
storeNode2FlowNode
|
||||
} from '@/web/core/workflow/utils';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
@@ -861,7 +861,7 @@ const WorkflowContextProvider = ({
|
||||
});
|
||||
const onSwitchCloudVersion = useMemoizedFn((appVersion: AppVersionSchemaType) => {
|
||||
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;
|
||||
|
||||
resetSnapshot({
|
||||
@@ -912,7 +912,7 @@ const WorkflowContextProvider = ({
|
||||
isInit?: boolean
|
||||
) => {
|
||||
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 数据存到内存并删除
|
||||
const pastSnapshot = (() => {
|
||||
|
@@ -2,6 +2,12 @@ import React, { type ReactNode, useEffect, useMemo, useRef, useState } from 'rea
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { useLocalStorageState } from 'ahooks';
|
||||
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 = {
|
||||
mouseInCanvas: boolean;
|
||||
@@ -20,6 +26,8 @@ type WorkflowEventContextType = {
|
||||
// version history
|
||||
showHistoryModal: boolean;
|
||||
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleParams: handleParamsType | null;
|
||||
setHandleParams: React.Dispatch<React.SetStateAction<handleParamsType | null>>;
|
||||
};
|
||||
|
||||
export const WorkflowEventContext = createContext<WorkflowEventContextType>({
|
||||
@@ -42,6 +50,10 @@ export const WorkflowEventContext = createContext<WorkflowEventContextType>({
|
||||
showHistoryModal: false,
|
||||
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
|
||||
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
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const [mouseInCanvas, setMouseInCanvas] = useState(false);
|
||||
const [handleParams, setHandleParams] = useState<handleParamsType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseInCanvas = (e: MouseEvent) => {
|
||||
setMouseInCanvas(true);
|
||||
@@ -62,7 +76,7 @@ const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) =>
|
||||
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
|
||||
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
|
||||
};
|
||||
}, [reactFlowWrapper?.current, setMouseInCanvas]);
|
||||
}, [setMouseInCanvas]);
|
||||
|
||||
// Watch hover node
|
||||
const [hoverNodeId, setHoverNodeId] = useState<string>();
|
||||
@@ -95,7 +109,9 @@ const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) =>
|
||||
menu,
|
||||
setMenu,
|
||||
showHistoryModal,
|
||||
setShowHistoryModal
|
||||
setShowHistoryModal,
|
||||
handleParams,
|
||||
setHandleParams
|
||||
}),
|
||||
[
|
||||
mouseInCanvas,
|
||||
@@ -108,7 +124,9 @@ const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) =>
|
||||
menu,
|
||||
setMenu,
|
||||
showHistoryModal,
|
||||
setShowHistoryModal
|
||||
setShowHistoryModal,
|
||||
handleParams,
|
||||
setHandleParams
|
||||
]
|
||||
);
|
||||
return (
|
||||
|
@@ -2,7 +2,7 @@ import { computedNodeInputReference } from '@/web/core/workflow/utils';
|
||||
import { type AppDetailType } from '@fastgpt/global/core/app/type';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
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 {
|
||||
type FlowNodeItemType,
|
||||
type StoreNodeItemType
|
||||
|
@@ -170,11 +170,16 @@ export const storeNode2FlowNode = ({
|
||||
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 {
|
||||
...edge,
|
||||
id: getNanoid(),
|
||||
type: EDGE_TYPE
|
||||
type: EDGE_TYPE,
|
||||
sourceHandle,
|
||||
targetHandle
|
||||
};
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user