perf: workflow context split (#3083)
* perf: workflow context split * perf: context
This commit is contained in:
@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
|
|||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
import SaveButton from '../Workflow/components/SaveButton';
|
import SaveButton from '../Workflow/components/SaveButton';
|
||||||
import PublishHistories from '../PublishHistoriesSlider';
|
import PublishHistories from '../PublishHistoriesSlider';
|
||||||
|
import {
|
||||||
|
WorkflowActionContext,
|
||||||
|
WorkflowInitContext
|
||||||
|
} from '../WorkflowComponents/context/workflowInitContext';
|
||||||
|
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -44,20 +49,24 @@ const Header = () => {
|
|||||||
onClose: onCloseBackConfirm
|
onClose: onCloseBackConfirm
|
||||||
} = useDisclosure();
|
} = useDisclosure();
|
||||||
|
|
||||||
|
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||||
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
const {
|
const {
|
||||||
flowData2StoreData,
|
flowData2StoreData,
|
||||||
flowData2StoreDataAndCheck,
|
flowData2StoreDataAndCheck,
|
||||||
setWorkflowTestData,
|
setWorkflowTestData,
|
||||||
setShowHistoryModal,
|
|
||||||
showHistoryModal,
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
past,
|
past,
|
||||||
future,
|
future,
|
||||||
setPast,
|
setPast,
|
||||||
onSwitchTmpVersion,
|
onSwitchTmpVersion,
|
||||||
onSwitchCloudVersion
|
onSwitchCloudVersion
|
||||||
} = useContextSelector(WorkflowContext, (v) => v);
|
} = useContextSelector(WorkflowContext, (v) => v);
|
||||||
|
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
|
||||||
|
const setShowHistoryModal = useContextSelector(
|
||||||
|
WorkflowEventContext,
|
||||||
|
(v) => v.setShowHistoryModal
|
||||||
|
);
|
||||||
|
|
||||||
const { lastAppListRouteType } = useSystemStore();
|
const { lastAppListRouteType } = useSystemStore();
|
||||||
|
|
||||||
const [isPublished, setIsPublished] = useState(false);
|
const [isPublished, setIsPublished] = useState(false);
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
|
|||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
import SaveButton from './components/SaveButton';
|
import SaveButton from './components/SaveButton';
|
||||||
import PublishHistories from '../PublishHistoriesSlider';
|
import PublishHistories from '../PublishHistoriesSlider';
|
||||||
|
import {
|
||||||
|
WorkflowActionContext,
|
||||||
|
WorkflowInitContext
|
||||||
|
} from '../WorkflowComponents/context/workflowInitContext';
|
||||||
|
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -48,20 +53,23 @@ const Header = () => {
|
|||||||
onClose: onCloseBackConfirm
|
onClose: onCloseBackConfirm
|
||||||
} = useDisclosure();
|
} = useDisclosure();
|
||||||
|
|
||||||
|
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||||
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
const {
|
const {
|
||||||
flowData2StoreData,
|
flowData2StoreData,
|
||||||
flowData2StoreDataAndCheck,
|
flowData2StoreDataAndCheck,
|
||||||
setWorkflowTestData,
|
setWorkflowTestData,
|
||||||
setShowHistoryModal,
|
|
||||||
showHistoryModal,
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
past,
|
past,
|
||||||
future,
|
future,
|
||||||
setPast,
|
setPast,
|
||||||
onSwitchTmpVersion,
|
onSwitchTmpVersion,
|
||||||
onSwitchCloudVersion
|
onSwitchCloudVersion
|
||||||
} = useContextSelector(WorkflowContext, (v) => v);
|
} = useContextSelector(WorkflowContext, (v) => v);
|
||||||
|
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
|
||||||
|
const setShowHistoryModal = useContextSelector(
|
||||||
|
WorkflowEventContext,
|
||||||
|
(v) => v.setShowHistoryModal
|
||||||
|
);
|
||||||
|
|
||||||
const { lastAppListRouteType } = useSystemStore();
|
const { lastAppListRouteType } = useSystemStore();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { cloneDeep } from 'lodash';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import Flow from '../WorkflowComponents/Flow';
|
import Flow from '../WorkflowComponents/Flow';
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowCustomProvider } from '../WorkflowComponents/context/index';
|
||||||
|
|
||||||
const Logs = dynamic(() => import('../Logs/index'));
|
const Logs = dynamic(() => import('../Logs/index'));
|
||||||
const PublishChannel = dynamic(() => import('../Publish'));
|
const PublishChannel = dynamic(() => import('../Publish'));
|
||||||
@@ -67,11 +67,9 @@ const WorkflowEdit = () => {
|
|||||||
|
|
||||||
const Render = () => {
|
const Render = () => {
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowCustomProvider templates={appSystemModuleTemplates}>
|
||||||
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
|
<WorkflowEdit />
|
||||||
<WorkflowEdit />
|
</ReactFlowCustomProvider>
|
||||||
</WorkflowContextProvider>
|
|
||||||
</ReactFlowProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { useUserStore } from '@/web/support/user/useUserStore';
|
|||||||
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
||||||
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
||||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
|
import { WorkflowActionContext } from '../context/workflowInitContext';
|
||||||
|
|
||||||
type ModuleTemplateListProps = {
|
type ModuleTemplateListProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -79,10 +80,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
|||||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||||
const [searchKey, setSearchKey] = useState('');
|
const [searchKey, setSearchKey] = useState('');
|
||||||
const { feConfigs } = useSystemStore();
|
const { feConfigs } = useSystemStore();
|
||||||
const { basicNodeTemplates, hasToolNode, nodeList, appId } = useContextSelector(
|
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
|
||||||
WorkflowContext,
|
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
|
||||||
(v) => v
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
);
|
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
|
||||||
|
|
||||||
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
|
||||||
manual: !feConfigs.isPlus
|
manual: !feConfigs.isPlus
|
||||||
@@ -217,105 +218,120 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const Render = useMemo(() => {
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
<Box
|
||||||
<Box
|
zIndex={2}
|
||||||
zIndex={2}
|
display={isOpen ? 'block' : 'none'}
|
||||||
display={isOpen ? 'block' : 'none'}
|
position={'absolute'}
|
||||||
position={'absolute'}
|
top={0}
|
||||||
top={0}
|
left={0}
|
||||||
left={0}
|
bottom={0}
|
||||||
bottom={0}
|
w={`${sliderWidth}px`}
|
||||||
w={`${sliderWidth}px`}
|
maxW={'100%'}
|
||||||
maxW={'100%'}
|
onClick={onClose}
|
||||||
onClick={onClose}
|
fontSize={'sm'}
|
||||||
fontSize={'sm'}
|
/>
|
||||||
/>
|
<MyBox
|
||||||
<MyBox
|
isLoading={isLoading}
|
||||||
isLoading={isLoading}
|
display={'flex'}
|
||||||
display={'flex'}
|
zIndex={3}
|
||||||
zIndex={3}
|
flexDirection={'column'}
|
||||||
flexDirection={'column'}
|
position={'absolute'}
|
||||||
position={'absolute'}
|
top={'10px'}
|
||||||
top={'10px'}
|
left={0}
|
||||||
left={0}
|
pt={'20px'}
|
||||||
pt={'20px'}
|
pb={4}
|
||||||
pb={4}
|
h={isOpen ? 'calc(100% - 20px)' : '0'}
|
||||||
h={isOpen ? 'calc(100% - 20px)' : '0'}
|
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
|
||||||
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
|
bg={'white'}
|
||||||
bg={'white'}
|
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
borderRadius={'0 20px 20px 0'}
|
||||||
borderRadius={'0 20px 20px 0'}
|
transition={'.2s ease'}
|
||||||
transition={'.2s ease'}
|
userSelect={'none'}
|
||||||
userSelect={'none'}
|
overflow={isOpen ? 'none' : 'hidden'}
|
||||||
overflow={isOpen ? 'none' : 'hidden'}
|
>
|
||||||
>
|
{/* Header */}
|
||||||
{/* Header */}
|
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||||
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
|
{/* Tabs */}
|
||||||
{/* Tabs */}
|
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
|
||||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
|
<Box flex={'1 0 0'}>
|
||||||
<Box flex={'1 0 0'}>
|
<FillRowTabs
|
||||||
<FillRowTabs
|
list={[
|
||||||
list={[
|
{
|
||||||
{
|
icon: 'core/modules/basicNode',
|
||||||
icon: 'core/modules/basicNode',
|
label: t('common:core.module.template.Basic Node'),
|
||||||
label: t('common:core.module.template.Basic Node'),
|
value: TemplateTypeEnum.basic
|
||||||
value: TemplateTypeEnum.basic
|
},
|
||||||
},
|
{
|
||||||
{
|
icon: 'core/modules/systemPlugin',
|
||||||
icon: 'core/modules/systemPlugin',
|
label: t('common:core.module.template.System Plugin'),
|
||||||
label: t('common:core.module.template.System Plugin'),
|
value: TemplateTypeEnum.systemPlugin
|
||||||
value: TemplateTypeEnum.systemPlugin
|
},
|
||||||
},
|
{
|
||||||
{
|
icon: 'core/modules/teamPlugin',
|
||||||
icon: 'core/modules/teamPlugin',
|
label: t('common:core.module.template.Team app'),
|
||||||
label: t('common:core.module.template.Team app'),
|
value: TemplateTypeEnum.teamPlugin
|
||||||
value: TemplateTypeEnum.teamPlugin
|
}
|
||||||
}
|
]}
|
||||||
]}
|
width={'100%'}
|
||||||
width={'100%'}
|
py={'5px'}
|
||||||
py={'5px'}
|
value={templateType}
|
||||||
value={templateType}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
loadNodeTemplates({
|
||||||
loadNodeTemplates({
|
type: e as TemplateTypeEnum,
|
||||||
type: e as TemplateTypeEnum,
|
parentId: ''
|
||||||
parentId: ''
|
});
|
||||||
});
|
}}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{/* close icon */}
|
|
||||||
<IconButton
|
|
||||||
size={'sm'}
|
|
||||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
|
|
||||||
borderColor={'myGray.300'}
|
|
||||||
variant={'grayBase'}
|
|
||||||
aria-label={''}
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Box>
|
||||||
{/* Search */}
|
{/* close icon */}
|
||||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
<IconButton
|
||||||
templateType === TemplateTypeEnum.systemPlugin) && (
|
size={'sm'}
|
||||||
<Flex mt={2} alignItems={'center'} h={10}>
|
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
|
||||||
<InputGroup mr={4} h={'full'}>
|
borderColor={'myGray.300'}
|
||||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
variant={'grayBase'}
|
||||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
aria-label={''}
|
||||||
</InputLeftElement>
|
onClick={onClose}
|
||||||
<Input
|
/>
|
||||||
h={'full'}
|
</Flex>
|
||||||
bg={'myGray.50'}
|
{/* Search */}
|
||||||
placeholder={
|
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||||
templateType === TemplateTypeEnum.teamPlugin
|
templateType === TemplateTypeEnum.systemPlugin) && (
|
||||||
? t('common:plugin.Search_app')
|
<Flex mt={2} alignItems={'center'} h={10}>
|
||||||
: t('common:plugin.Search plugin')
|
<InputGroup mr={4} h={'full'}>
|
||||||
}
|
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||||
onChange={(e) => setSearchKey(e.target.value)}
|
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||||
/>
|
</InputLeftElement>
|
||||||
</InputGroup>
|
<Input
|
||||||
<Box flex={1} />
|
h={'full'}
|
||||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
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('/app/list')}
|
||||||
|
gap={1}
|
||||||
|
>
|
||||||
|
<Box>{t('common:create')}</Box>
|
||||||
|
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{templateType === TemplateTypeEnum.systemPlugin &&
|
||||||
|
feConfigs.systemPluginCourseUrl && (
|
||||||
<Flex
|
<Flex
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
@@ -323,68 +339,35 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
|||||||
color: 'primary.600'
|
color: 'primary.600'
|
||||||
}}
|
}}
|
||||||
fontSize={'sm'}
|
fontSize={'sm'}
|
||||||
onClick={() => router.push('/app/list')}
|
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
|
||||||
gap={1}
|
gap={1}
|
||||||
>
|
>
|
||||||
<Box>{t('common:create')}</Box>
|
<Box>{t('common:plugin.contribute')}</Box>
|
||||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{templateType === TemplateTypeEnum.systemPlugin &&
|
</Flex>
|
||||||
feConfigs.systemPluginCourseUrl && (
|
)}
|
||||||
<Flex
|
{/* paths */}
|
||||||
alignItems={'center'}
|
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||||
cursor={'pointer'}
|
templateType === TemplateTypeEnum.systemPlugin) &&
|
||||||
_hover={{
|
!searchKey &&
|
||||||
color: 'primary.600'
|
parentId && (
|
||||||
}}
|
<Flex alignItems={'center'} mt={2}>
|
||||||
fontSize={'sm'}
|
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
||||||
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
|
|
||||||
gap={1}
|
|
||||||
>
|
|
||||||
<Box>{t('common:plugin.contribute')}</Box>
|
|
||||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{/* paths */}
|
</Box>
|
||||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
<RenderList
|
||||||
templateType === TemplateTypeEnum.systemPlugin) &&
|
templates={templates}
|
||||||
!searchKey &&
|
type={templateType}
|
||||||
parentId && (
|
onClose={onClose}
|
||||||
<Flex alignItems={'center'} mt={2}>
|
parentId={parentId}
|
||||||
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
setParentId={onUpdateParentId}
|
||||||
</Flex>
|
/>
|
||||||
)}
|
</MyBox>
|
||||||
</Box>
|
</>
|
||||||
<RenderList
|
);
|
||||||
templates={templates}
|
|
||||||
type={templateType}
|
|
||||||
onClose={onClose}
|
|
||||||
parentId={parentId}
|
|
||||||
setParentId={onUpdateParentId}
|
|
||||||
/>
|
|
||||||
</MyBox>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
isLoading,
|
|
||||||
t,
|
|
||||||
templateType,
|
|
||||||
feConfigs.systemPluginCourseUrl,
|
|
||||||
searchKey,
|
|
||||||
parentId,
|
|
||||||
paths,
|
|
||||||
onUpdateParentId,
|
|
||||||
templates,
|
|
||||||
loadNodeTemplates,
|
|
||||||
router
|
|
||||||
]);
|
|
||||||
|
|
||||||
return Render;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(NodeTemplatesModal);
|
export default React.memo(NodeTemplatesModal);
|
||||||
@@ -403,9 +386,11 @@ const RenderList = React.memo(function RenderList({
|
|||||||
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
|
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
|
||||||
|
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
const { toast } = useToast();
|
|
||||||
const { reactFlowWrapper, setNodes, nodeList } = useContextSelector(WorkflowContext, (v) => v);
|
|
||||||
const { computedNewNodeName } = useWorkflowUtils();
|
const { computedNewNodeName } = useWorkflowUtils();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
|
||||||
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
|
|
||||||
const formatTemplates = useMemo<NodeTemplateListType>(() => {
|
const formatTemplates = useMemo<NodeTemplateListType>(() => {
|
||||||
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
|
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
|
||||||
@@ -426,8 +411,6 @@ const RenderList = React.memo(function RenderList({
|
|||||||
template: NodeTemplateListItemType;
|
template: NodeTemplateListItemType;
|
||||||
position: XYPosition;
|
position: XYPosition;
|
||||||
}) => {
|
}) => {
|
||||||
if (!reactFlowWrapper?.current) return;
|
|
||||||
|
|
||||||
// Load template node
|
// Load template node
|
||||||
const templateNode = await (async () => {
|
const templateNode = await (async () => {
|
||||||
try {
|
try {
|
||||||
@@ -539,16 +522,7 @@ const RenderList = React.memo(function RenderList({
|
|||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[screenToFlowPosition, nodeList, computedNewNodeName, t, setNodes, setLoading, toast]
|
||||||
reactFlowWrapper,
|
|
||||||
screenToFlowPosition,
|
|
||||||
nodeList,
|
|
||||||
computedNewNodeName,
|
|
||||||
t,
|
|
||||||
setNodes,
|
|
||||||
setLoading,
|
|
||||||
toast
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const gridStyle = useMemo(() => {
|
const gridStyle = useMemo(() => {
|
||||||
@@ -571,133 +545,118 @@ const RenderList = React.memo(function RenderList({
|
|||||||
};
|
};
|
||||||
}, [type]);
|
}, [type]);
|
||||||
|
|
||||||
const Render = useMemo(() => {
|
return templates.length === 0 ? (
|
||||||
return templates.length === 0 ? (
|
<EmptyTip text={t('app:module.No Modules')} />
|
||||||
<EmptyTip text={t('app:module.No Modules')} />
|
) : (
|
||||||
) : (
|
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
|
||||||
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
|
<Box mx={'auto'}>
|
||||||
<Box mx={'auto'}>
|
{formatTemplates.map((item, i) => (
|
||||||
{formatTemplates.map((item, i) => (
|
<Box
|
||||||
<Box
|
key={item.type}
|
||||||
key={item.type}
|
css={css({
|
||||||
css={css({
|
span: {
|
||||||
span: {
|
display: 'block'
|
||||||
display: 'block'
|
}
|
||||||
}
|
})}
|
||||||
})}
|
_notLast={{ mb: 5 }}
|
||||||
_notLast={{ mb: 5 }}
|
>
|
||||||
>
|
{item.label && formatTemplates.length > 1 && (
|
||||||
{item.label && formatTemplates.length > 1 && (
|
<Flex>
|
||||||
<Flex>
|
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||||
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
{t(item.label as any)}
|
||||||
{t(item.label as any)}
|
</Box>
|
||||||
</Box>
|
</Flex>
|
||||||
</Flex>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
|
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
|
||||||
{item.list.map((template) => (
|
{item.list.map((template) => (
|
||||||
<MyTooltip
|
<MyTooltip
|
||||||
key={template.id}
|
key={template.id}
|
||||||
placement={'right'}
|
placement={'right'}
|
||||||
label={
|
label={
|
||||||
<Box py={2}>
|
<Box py={2}>
|
||||||
<Flex alignItems={'center'}>
|
<Flex alignItems={'center'}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={template.avatar}
|
src={template.avatar}
|
||||||
w={'1.75rem'}
|
w={'1.75rem'}
|
||||||
objectFit={'contain'}
|
objectFit={'contain'}
|
||||||
borderRadius={'sm'}
|
borderRadius={'sm'}
|
||||||
/>
|
/>
|
||||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
||||||
{t(template.name as any)}
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
<Box mt={2} color={'myGray.500'}>
|
|
||||||
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
|
||||||
</Box>
|
|
||||||
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
alignItems={'center'}
|
|
||||||
py={gridStyle.py}
|
|
||||||
px={3}
|
|
||||||
cursor={'pointer'}
|
|
||||||
_hover={{ bg: 'myWhite.600' }}
|
|
||||||
borderRadius={'sm'}
|
|
||||||
draggable={!template.isFolder}
|
|
||||||
onDragEnd={(e) => {
|
|
||||||
if (e.clientX < sliderWidth) return;
|
|
||||||
onAddNode({
|
|
||||||
template,
|
|
||||||
position: { x: e.clientX, y: e.clientY }
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (template.isFolder) {
|
|
||||||
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();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
src={template.avatar}
|
|
||||||
w={gridStyle.avatarSize}
|
|
||||||
objectFit={'contain'}
|
|
||||||
borderRadius={'sm'}
|
|
||||||
/>
|
|
||||||
<Box ml={3} flex={'1'}>
|
|
||||||
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
|
|
||||||
{t(template.name as any)}
|
{t(template.name as any)}
|
||||||
</Box>
|
</Box>
|
||||||
{gridStyle.authorInName && template.author !== undefined && (
|
</Flex>
|
||||||
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
|
<Box mt={2} color={'myGray.500'}>
|
||||||
{`by ${template.author || feConfigs.systemTitle}`}
|
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
|
||||||
{gridStyle.authorInRight && template.authorAvatar && template.author && (
|
</Box>
|
||||||
<HStack spacing={1} maxW={'120px'}>
|
}
|
||||||
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
>
|
||||||
<Box fontSize={'xs'} className="textEllipsis">
|
<Flex
|
||||||
{template.author}
|
alignItems={'center'}
|
||||||
</Box>
|
py={gridStyle.py}
|
||||||
</HStack>
|
px={3}
|
||||||
|
cursor={'pointer'}
|
||||||
|
_hover={{ bg: 'myWhite.600' }}
|
||||||
|
borderRadius={'sm'}
|
||||||
|
draggable={!template.isFolder}
|
||||||
|
onDragEnd={(e) => {
|
||||||
|
if (e.clientX < sliderWidth) return;
|
||||||
|
onAddNode({
|
||||||
|
template,
|
||||||
|
position: { x: e.clientX, y: e.clientY }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (template.isFolder) {
|
||||||
|
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();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={template.avatar}
|
||||||
|
w={gridStyle.avatarSize}
|
||||||
|
objectFit={'contain'}
|
||||||
|
borderRadius={'sm'}
|
||||||
|
/>
|
||||||
|
<Box ml={3} flex={'1'}>
|
||||||
|
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
|
||||||
|
{t(template.name as any)}
|
||||||
|
</Box>
|
||||||
|
{gridStyle.authorInName && template.author !== undefined && (
|
||||||
|
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
|
||||||
|
{`by ${template.author || feConfigs.systemTitle}`}
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Box>
|
||||||
</MyTooltip>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
feConfigs.systemTitle,
|
|
||||||
formatTemplates,
|
|
||||||
gridStyle,
|
|
||||||
isPc,
|
|
||||||
isSystemPlugin,
|
|
||||||
onAddNode,
|
|
||||||
onClose,
|
|
||||||
setParentId,
|
|
||||||
t,
|
|
||||||
templates.length
|
|
||||||
]);
|
|
||||||
|
|
||||||
return Render;
|
{gridStyle.authorInRight && template.authorAvatar && template.author && (
|
||||||
|
<HStack spacing={1} maxW={'120px'}>
|
||||||
|
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
||||||
|
<Box fontSize={'xs'} className="textEllipsis">
|
||||||
|
{template.author}
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</MyTooltip>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/w
|
|||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../context';
|
import { WorkflowContext } from '../../context';
|
||||||
import { useThrottleEffect } from 'ahooks';
|
import { useThrottleEffect } from 'ahooks';
|
||||||
|
import { WorkflowActionContext, WorkflowInitContext } from '../../context/workflowInitContext';
|
||||||
|
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||||
|
|
||||||
const ButtonEdge = (props: EdgeProps) => {
|
const ButtonEdge = (props: EdgeProps) => {
|
||||||
const { nodes, nodeList, onEdgesChange, workflowDebugData, hoverEdgeId } = useContextSelector(
|
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||||
WorkflowContext,
|
const onEdgesChange = useContextSelector(WorkflowActionContext, (v) => v.onEdgesChange);
|
||||||
(v) => v
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
);
|
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
|
||||||
|
const hoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.hoverEdgeId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -7,76 +7,76 @@ import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comme
|
|||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../context';
|
import { WorkflowContext } from '../../context';
|
||||||
import { useReactFlow } from 'reactflow';
|
import { useReactFlow } from 'reactflow';
|
||||||
|
import { WorkflowActionContext } from '../../context/workflowInitContext';
|
||||||
|
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||||
|
|
||||||
type ContextMenuProps = {
|
const ContextMenu = () => {
|
||||||
top: number;
|
|
||||||
left: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ContextMenu = ({ top, left }: ContextMenuProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const setNodes = useContextSelector(WorkflowContext, (ctx) => ctx.setNodes);
|
const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
|
||||||
const setMenu = useContextSelector(WorkflowContext, (ctx) => ctx.setMenu);
|
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
|
||||||
|
const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu);
|
||||||
|
|
||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
const newNode = nodeTemplate2FlowNode({
|
const newNode = nodeTemplate2FlowNode({
|
||||||
template: CommentNode,
|
template: CommentNode,
|
||||||
position: screenToFlowPosition({ x: left, y: top }),
|
position: screenToFlowPosition({ x: menu?.left ?? 0, y: menu?.top ?? 0 }),
|
||||||
t
|
t
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative">
|
!!menu && (
|
||||||
<Box
|
<Box position="relative">
|
||||||
position="absolute"
|
<Box
|
||||||
top={`${top - 6}px`}
|
position="absolute"
|
||||||
left={`${left + 10}px`}
|
top={`${menu.top - 6}px`}
|
||||||
width={0}
|
left={`${menu.left + 10}px`}
|
||||||
height={0}
|
width={0}
|
||||||
borderLeft="6px solid transparent"
|
height={0}
|
||||||
borderRight="6px solid transparent"
|
borderLeft="6px solid transparent"
|
||||||
borderBottom="6px solid white"
|
borderRight="6px solid transparent"
|
||||||
zIndex={2}
|
borderBottom="6px solid white"
|
||||||
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
|
zIndex={2}
|
||||||
/>
|
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
|
||||||
<Flex
|
/>
|
||||||
position={'absolute'}
|
<Flex
|
||||||
top={top}
|
position={'absolute'}
|
||||||
left={left}
|
top={menu.top}
|
||||||
bg={'white'}
|
left={menu.left}
|
||||||
w={'120px'}
|
bg={'white'}
|
||||||
height={9}
|
w={'120px'}
|
||||||
p={1}
|
height={9}
|
||||||
rounded={'md'}
|
p={1}
|
||||||
boxShadow={'0px 2px 4px 0px #A1A7B340'}
|
rounded={'md'}
|
||||||
className="context-menu"
|
boxShadow={'0px 2px 4px 0px #A1A7B340'}
|
||||||
alignItems={'center'}
|
className="context-menu"
|
||||||
color={'myGray.600'}
|
alignItems={'center'}
|
||||||
cursor={'pointer'}
|
color={'myGray.600'}
|
||||||
_hover={{
|
cursor={'pointer'}
|
||||||
color: 'primary.500'
|
_hover={{
|
||||||
}}
|
color: 'primary.500'
|
||||||
onClick={() => {
|
}}
|
||||||
setMenu(null);
|
onClick={() => {
|
||||||
setNodes((state) => {
|
setMenu(null);
|
||||||
const newState = state
|
setNodes((state) => {
|
||||||
.map((node) => ({
|
const newState = state
|
||||||
...node,
|
.map((node) => ({
|
||||||
selected: false
|
...node,
|
||||||
}))
|
selected: false
|
||||||
// @ts-ignore
|
}))
|
||||||
.concat(newNode);
|
// @ts-ignore
|
||||||
return newState;
|
.concat(newNode);
|
||||||
});
|
return newState;
|
||||||
}}
|
});
|
||||||
zIndex={1}
|
}}
|
||||||
>
|
zIndex={1}
|
||||||
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
|
>
|
||||||
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
|
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
|
||||||
{t('workflow:context_menu.add_comment')}
|
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
|
||||||
</Box>
|
{t('workflow:context_menu.add_comment')}
|
||||||
</Flex>
|
</Box>
|
||||||
</Box>
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
|||||||
import { Box } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
import { maxZoom, minZoom } from '../index';
|
import { maxZoom, minZoom } from '../../constants';
|
||||||
import { useKeyPress } from 'ahooks';
|
import { useKeyPress } from 'ahooks';
|
||||||
|
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||||
|
|
||||||
const buttonStyle = {
|
const buttonStyle = {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -27,16 +28,20 @@ const buttonStyle = {
|
|||||||
const FlowController = React.memo(function FlowController() {
|
const FlowController = React.memo(function FlowController() {
|
||||||
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
||||||
const { zoom } = useViewport();
|
const { zoom } = useViewport();
|
||||||
const {
|
const undo = useContextSelector(WorkflowContext, (v) => v.undo);
|
||||||
undo,
|
const redo = useContextSelector(WorkflowContext, (v) => v.redo);
|
||||||
redo,
|
const canRedo = useContextSelector(WorkflowContext, (v) => v.canRedo);
|
||||||
canRedo,
|
const canUndo = useContextSelector(WorkflowContext, (v) => v.canUndo);
|
||||||
canUndo,
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
workflowControlMode,
|
const workflowControlMode = useContextSelector(
|
||||||
setWorkflowControlMode,
|
WorkflowEventContext,
|
||||||
mouseInCanvas,
|
(v) => v.workflowControlMode
|
||||||
nodeList
|
);
|
||||||
} = useContextSelector(WorkflowContext, (v) => v);
|
const setWorkflowControlMode = useContextSelector(
|
||||||
|
WorkflowEventContext,
|
||||||
|
(v) => v.setWorkflowControlMode
|
||||||
|
);
|
||||||
|
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
|
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, StackProps, HStack } from '@chakra-ui/react';
|
import { Box, StackProps, HStack } from '@chakra-ui/react';
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
|
||||||
|
|
||||||
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => {
|
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
|
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||||
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||||
import { useCallback, useState, useMemo, useEffect } from 'react';
|
import { useCallback, useState, useMemo } from 'react';
|
||||||
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
|
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||||
@@ -27,13 +27,14 @@ import {
|
|||||||
} from '@fastgpt/global/core/workflow/constants';
|
} from '@fastgpt/global/core/workflow/constants';
|
||||||
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
|
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext, getWorkflowStore } from '../../context';
|
import { WorkflowContext } from '../../context';
|
||||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
import { AppContext } from '../../../context';
|
import { AppContext } from '../../../context';
|
||||||
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
|
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
|
||||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||||
|
import { WorkflowActionContext } from '../../context/workflowInitContext';
|
||||||
|
|
||||||
const MyRightDrawer = dynamic(
|
const MyRightDrawer = dynamic(
|
||||||
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
|
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
|
||||||
@@ -49,9 +50,10 @@ export const useDebug = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
|
||||||
|
const getNodes = useContextSelector(WorkflowActionContext, (v) => v.getNodes);
|
||||||
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
||||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
|
||||||
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
|
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
|
||||||
|
|
||||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||||
@@ -76,7 +78,7 @@ export const useDebug = () => {
|
|||||||
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
|
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
|
||||||
|
|
||||||
const flowData2StoreDataAndCheck = useCallback(async () => {
|
const flowData2StoreDataAndCheck = useCallback(async () => {
|
||||||
const { nodes } = await getWorkflowStore();
|
const nodes = getNodes();
|
||||||
|
|
||||||
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||||
if (!checkResults) {
|
if (!checkResults) {
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { Node, useKeyPress } from 'reactflow';
|
import { Node, useKeyPress } from 'reactflow';
|
||||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext, getWorkflowStore } from '../../context';
|
|
||||||
import { useWorkflowUtils } from './useUtils';
|
import { useWorkflowUtils } from './useUtils';
|
||||||
import { useKeyPress as useKeyPressEffect } from 'ahooks';
|
import { useKeyPress as useKeyPressEffect } from 'ahooks';
|
||||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
|
import { WorkflowActionContext } from '../../context/workflowInitContext';
|
||||||
|
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||||
|
|
||||||
export const useKeyboard = () => {
|
export const useKeyboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setNodes, mouseInCanvas } = useContextSelector(WorkflowContext, (v) => v);
|
const getNodes = useContextSelector(WorkflowActionContext, (v) => v.getNodes);
|
||||||
|
const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
|
||||||
|
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
|
||||||
|
|
||||||
const { copyData } = useCopyData();
|
const { copyData } = useCopyData();
|
||||||
const { computedNewNodeName } = useWorkflowUtils();
|
const { computedNewNodeName } = useWorkflowUtils();
|
||||||
|
|
||||||
@@ -33,14 +37,14 @@ export const useKeyboard = () => {
|
|||||||
|
|
||||||
const onCopy = useCallback(async () => {
|
const onCopy = useCallback(async () => {
|
||||||
if (hasInputtingElement()) return;
|
if (hasInputtingElement()) return;
|
||||||
const { nodes } = await getWorkflowStore();
|
const nodes = getNodes();
|
||||||
|
|
||||||
const selectedNodes = nodes.filter(
|
const selectedNodes = nodes.filter(
|
||||||
(node) => node.selected && !node.data?.isError && node.data?.unique !== true
|
(node) => node.selected && !node.data?.isError && node.data?.unique !== true
|
||||||
);
|
);
|
||||||
if (selectedNodes.length === 0) return;
|
if (selectedNodes.length === 0) return;
|
||||||
copyData(JSON.stringify(selectedNodes), t('common:core.workflow.Copy node'));
|
copyData(JSON.stringify(selectedNodes), t('common:core.workflow.Copy node'));
|
||||||
}, [copyData, hasInputtingElement, t]);
|
}, [copyData, getNodes, hasInputtingElement, t]);
|
||||||
|
|
||||||
const onParse = useCallback(async () => {
|
const onParse = useCallback(async () => {
|
||||||
if (hasInputtingElement()) return;
|
if (hasInputtingElement()) return;
|
||||||
|
|||||||
@@ -25,12 +25,16 @@ import { useContextSelector } from 'use-context-selector';
|
|||||||
import { WorkflowContext } from '../../context';
|
import { WorkflowContext } from '../../context';
|
||||||
import { THelperLine } from '@fastgpt/global/core/workflow/type';
|
import { THelperLine } from '@fastgpt/global/core/workflow/type';
|
||||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
import { useMemoizedFn } from 'ahooks';
|
import { useDebounceEffect, useMemoizedFn } from 'ahooks';
|
||||||
import {
|
import {
|
||||||
Input_Template_Node_Height,
|
Input_Template_Node_Height,
|
||||||
Input_Template_Node_Width
|
Input_Template_Node_Width
|
||||||
} from '@fastgpt/global/core/workflow/template/input';
|
} from '@fastgpt/global/core/workflow/template/input';
|
||||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||||
|
import { WorkflowActionContext, WorkflowInitContext } from '../../context/workflowInitContext';
|
||||||
|
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
|
||||||
|
import { AppContext } from '../../../context';
|
||||||
|
import { WorkflowEventContext } from '../../context/workflowEventContext';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Compute helper lines for snapping nodes to each other
|
Compute helper lines for snapping nodes to each other
|
||||||
@@ -271,21 +275,22 @@ export const useWorkflow = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { isDowningCtrl } = useKeyboard();
|
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
|
||||||
const {
|
|
||||||
setConnectingEdge,
|
const nodes = useContextSelector(WorkflowInitContext, (state) => state.nodes);
|
||||||
edges,
|
const onNodesChange = useContextSelector(WorkflowActionContext, (state) => state.onNodesChange);
|
||||||
nodes,
|
const edges = useContextSelector(WorkflowActionContext, (state) => state.edges);
|
||||||
nodeList,
|
const setEdges = useContextSelector(WorkflowActionContext, (v) => v.setEdges);
|
||||||
onNodesChange,
|
const onEdgesChange = useContextSelector(WorkflowActionContext, (v) => v.onEdgesChange);
|
||||||
setEdges,
|
const { setConnectingEdge, nodeList, onChangeNode, pushPastSnapshot } = useContextSelector(
|
||||||
onChangeNode,
|
WorkflowContext,
|
||||||
onEdgesChange,
|
(v) => v
|
||||||
setHoverEdgeId,
|
);
|
||||||
setMenu
|
const setHoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverEdgeId);
|
||||||
} = useContextSelector(WorkflowContext, (v) => v);
|
const setMenu = useContextSelector(WorkflowEventContext, (v) => v.setMenu);
|
||||||
|
|
||||||
const { getIntersectingNodes } = useReactFlow();
|
const { getIntersectingNodes } = useReactFlow();
|
||||||
|
const { isDowningCtrl } = useKeyboard();
|
||||||
|
|
||||||
// Loop node size and position
|
// Loop node size and position
|
||||||
const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => {
|
const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => {
|
||||||
@@ -330,41 +335,43 @@ export const useWorkflow = () => {
|
|||||||
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>();
|
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>();
|
||||||
|
|
||||||
const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => {
|
const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => {
|
||||||
const positionChange = change.type === 'position' && change.dragging ? change : undefined;
|
requestAnimationFrame(() => {
|
||||||
|
const positionChange = change.type === 'position' && change.dragging ? change : undefined;
|
||||||
|
|
||||||
if (positionChange?.position) {
|
if (positionChange?.position) {
|
||||||
// 只判断,3000px 内的 nodes,并按从近到远的顺序排序
|
// 只判断,3000px 内的 nodes,并按从近到远的顺序排序
|
||||||
const filterNodes = nodes
|
const filterNodes = nodes
|
||||||
.filter((node) => {
|
.filter((node) => {
|
||||||
if (!positionChange.position) return false;
|
if (!positionChange.position) return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Math.abs(node.position.x - positionChange.position.x) <= 3000 &&
|
Math.abs(node.position.x - positionChange.position.x) <= 3000 &&
|
||||||
Math.abs(node.position.y - positionChange.position.y) <= 3000
|
Math.abs(node.position.y - positionChange.position.y) <= 3000
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (!positionChange.position) return 0;
|
if (!positionChange.position) return 0;
|
||||||
return (
|
return (
|
||||||
Math.abs(a.position.x - positionChange.position.x) +
|
Math.abs(a.position.x - positionChange.position.x) +
|
||||||
Math.abs(a.position.y - positionChange.position.y) -
|
Math.abs(a.position.y - positionChange.position.y) -
|
||||||
Math.abs(b.position.x - positionChange.position.x) -
|
Math.abs(b.position.x - positionChange.position.x) -
|
||||||
Math.abs(b.position.y - positionChange.position.y)
|
Math.abs(b.position.y - positionChange.position.y)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.slice(0, 15);
|
.slice(0, 15);
|
||||||
|
|
||||||
const helperLines = computeHelperLines(positionChange, filterNodes);
|
const helperLines = computeHelperLines(positionChange, filterNodes);
|
||||||
|
|
||||||
positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x;
|
positionChange.position.x = helperLines.snapPosition.x ?? positionChange.position.x;
|
||||||
positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y;
|
positionChange.position.y = helperLines.snapPosition.y ?? positionChange.position.y;
|
||||||
|
|
||||||
setHelperLineHorizontal(helperLines.horizontal);
|
setHelperLineHorizontal(helperLines.horizontal);
|
||||||
setHelperLineVertical(helperLines.vertical);
|
setHelperLineVertical(helperLines.vertical);
|
||||||
} else {
|
} else {
|
||||||
setHelperLineHorizontal(undefined);
|
setHelperLineHorizontal(undefined);
|
||||||
setHelperLineVertical(undefined);
|
setHelperLineVertical(undefined);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if a node is placed on top of a loop node
|
// Check if a node is placed on top of a loop node
|
||||||
@@ -642,6 +649,23 @@ export const useWorkflow = () => {
|
|||||||
setMenu(null);
|
setMenu(null);
|
||||||
}, [setMenu]);
|
}, [setMenu]);
|
||||||
|
|
||||||
|
// Watch
|
||||||
|
// Auto save snapshot
|
||||||
|
useDebounceEffect(
|
||||||
|
() => {
|
||||||
|
if (nodes.length === 0 || !appDetail.chatConfig) return;
|
||||||
|
|
||||||
|
pushPastSnapshot({
|
||||||
|
pastNodes: nodes,
|
||||||
|
pastEdges: edges,
|
||||||
|
customTitle: formatTime2YMDHMS(new Date()),
|
||||||
|
chatConfig: appDetail.chatConfig
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[nodes, edges, appDetail.chatConfig],
|
||||||
|
{ wait: 500 }
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleNodesChange,
|
handleNodesChange,
|
||||||
handleEdgeChange,
|
handleEdgeChange,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow';
|
import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow';
|
||||||
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
|
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
|
||||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||||
@@ -11,16 +11,15 @@ import NodeTemplatesModal from './NodeTemplatesModal';
|
|||||||
|
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||||
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
|
import { connectionLineStyle, defaultEdgeOptions, maxZoom, minZoom } from '../constants';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../context';
|
import { WorkflowContext } from '../context';
|
||||||
import { useWorkflow } from './hooks/useWorkflow';
|
import { useWorkflow } from './hooks/useWorkflow';
|
||||||
import HelperLines from './components/HelperLines';
|
import HelperLines from './components/HelperLines';
|
||||||
import FlowController from './components/FlowController';
|
import FlowController from './components/FlowController';
|
||||||
import ContextMenu from './components/ContextMenu';
|
import ContextMenu from './components/ContextMenu';
|
||||||
|
import { WorkflowActionContext, WorkflowInitContext } from '../context/workflowInitContext';
|
||||||
export const minZoom = 0.1;
|
import { WorkflowEventContext } from '../context/workflowEventContext';
|
||||||
export const maxZoom = 1.5;
|
|
||||||
|
|
||||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||||
@@ -66,9 +65,12 @@ const edgeTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Workflow = () => {
|
const Workflow = () => {
|
||||||
const { nodes, edges, menu, reactFlowWrapper, workflowControlMode } = useContextSelector(
|
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||||
WorkflowContext,
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
(v) => v
|
const reactFlowWrapper = useContextSelector(WorkflowEventContext, (v) => v.reactFlowWrapper);
|
||||||
|
const workflowControlMode = useContextSelector(
|
||||||
|
WorkflowEventContext,
|
||||||
|
(v) => v.workflowControlMode
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -125,7 +127,7 @@ const Workflow = () => {
|
|||||||
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
{menu && <ContextMenu {...menu} />}
|
<ContextMenu />
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
ref={reactFlowWrapper}
|
ref={reactFlowWrapper}
|
||||||
fitView
|
fitView
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
|||||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
import { getEditorVariables } from '../../../utils';
|
import { getEditorVariables } from '../../../utils';
|
||||||
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||||
|
import { WorkflowActionContext } from '../../../context/workflowInitContext';
|
||||||
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
|
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
|
||||||
|
|
||||||
const defaultFormBody = {
|
const defaultFormBody = {
|
||||||
@@ -80,9 +81,10 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
|
||||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||||
|
|
||||||
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
|
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
|
||||||
@@ -256,8 +258,9 @@ export function RenderHttpProps({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
|
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
|
||||||
|
|
||||||
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
|
||||||
|
|
||||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||||
|
|
||||||
|
|||||||
@@ -53,24 +53,28 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
[chatConfig, setAppDetail]
|
[chatConfig, setAppDetail]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const Render = useMemo(() => {
|
||||||
<NodeCard
|
return (
|
||||||
selected={selected}
|
<NodeCard
|
||||||
menuForbid={{
|
selected={selected}
|
||||||
debug: true,
|
menuForbid={{
|
||||||
copy: true,
|
debug: true,
|
||||||
delete: true
|
copy: true,
|
||||||
}}
|
delete: true
|
||||||
{...data}
|
}}
|
||||||
>
|
{...data}
|
||||||
<Container w={'360px'}>
|
>
|
||||||
<Instruction {...componentsProps} />
|
<Container w={'360px'}>
|
||||||
<Box pt={4}>
|
<Instruction {...componentsProps} />
|
||||||
<FileSelectConfig {...componentsProps} />
|
<Box pt={4}>
|
||||||
</Box>
|
<FileSelectConfig {...componentsProps} />
|
||||||
</Container>
|
</Box>
|
||||||
</NodeCard>
|
</Container>
|
||||||
);
|
</NodeCard>
|
||||||
|
);
|
||||||
|
}, [componentsProps, data, selected]);
|
||||||
|
|
||||||
|
return Render;
|
||||||
};
|
};
|
||||||
export default React.memo(NodePluginConfig);
|
export default React.memo(NodePluginConfig);
|
||||||
|
|
||||||
@@ -116,8 +120,10 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
|
|||||||
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
|
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||||
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const pluginInputNode = nodes.find((item) => item.type === FlowNodeTypeEnum.pluginInput)!;
|
const pluginInputNode = nodeList.find(
|
||||||
|
(item) => item.flowNodeType === FlowNodeTypeEnum.pluginInput
|
||||||
|
)!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -137,7 +143,7 @@ function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: Co
|
|||||||
|
|
||||||
// Dynamic add or delete userFilesInput
|
// Dynamic add or delete userFilesInput
|
||||||
const canUploadFiles = e.canSelectFile || e.canSelectImg;
|
const canUploadFiles = e.canSelectFile || e.canSelectImg;
|
||||||
const repeatKey = pluginInputNode?.data.outputs.find(
|
const repeatKey = pluginInputNode?.outputs.find(
|
||||||
(item) => item.key === userFilesInput.key
|
(item) => item.key === userFilesInput.key
|
||||||
);
|
);
|
||||||
if (canUploadFiles) {
|
if (canUploadFiles) {
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ type ComponentProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||||
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
|
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||||
|
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
|
||||||
|
|
||||||
const chatConfig = useMemo<AppChatConfigType>(() => {
|
const chatConfig = useMemo<AppChatConfigType>(() => {
|
||||||
return getAppChatConfig({
|
return getAppChatConfig({
|
||||||
@@ -47,45 +48,49 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
[chatConfig, setAppDetail]
|
[chatConfig, setAppDetail]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const Render = useMemo(() => {
|
||||||
<>
|
return (
|
||||||
<NodeCard
|
<>
|
||||||
minW={'420px'}
|
<NodeCard
|
||||||
selected={selected}
|
minW={'420px'}
|
||||||
menuForbid={{
|
selected={selected}
|
||||||
debug: true,
|
menuForbid={{
|
||||||
copy: true,
|
debug: true,
|
||||||
delete: true
|
copy: true,
|
||||||
}}
|
delete: true
|
||||||
{...data}
|
}}
|
||||||
>
|
{...data}
|
||||||
<Container>
|
>
|
||||||
<WelcomeText {...componentsProps} />
|
<Container>
|
||||||
<Box mt={2} pt={2}>
|
<WelcomeText {...componentsProps} />
|
||||||
<ChatStartVariable {...componentsProps} />
|
<Box mt={2} pt={2}>
|
||||||
</Box>
|
<ChatStartVariable {...componentsProps} />
|
||||||
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
</Box>
|
||||||
<FileSelectConfig {...componentsProps} />
|
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||||
</Box>
|
<FileSelectConfig {...componentsProps} />
|
||||||
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
</Box>
|
||||||
<TTSGuide {...componentsProps} />
|
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||||
</Box>
|
<TTSGuide {...componentsProps} />
|
||||||
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
</Box>
|
||||||
<WhisperGuide {...componentsProps} />
|
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||||
</Box>
|
<WhisperGuide {...componentsProps} />
|
||||||
<Box mt={3} pt={4} borderTop={'base'} borderColor={'myGray.200'}>
|
</Box>
|
||||||
<QuestionGuide {...componentsProps} />
|
<Box mt={3} pt={4} borderTop={'base'} borderColor={'myGray.200'}>
|
||||||
</Box>
|
<QuestionGuide {...componentsProps} />
|
||||||
<Box mt={4} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
</Box>
|
||||||
<ScheduledTrigger {...componentsProps} />
|
<Box mt={4} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||||
</Box>
|
<ScheduledTrigger {...componentsProps} />
|
||||||
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
</Box>
|
||||||
<QuestionInputGuide {...componentsProps} />
|
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
|
||||||
</Box>
|
<QuestionInputGuide {...componentsProps} />
|
||||||
</Container>
|
</Box>
|
||||||
</NodeCard>
|
</Container>
|
||||||
</>
|
</NodeCard>
|
||||||
);
|
</>
|
||||||
|
);
|
||||||
|
}, [componentsProps, data, selected]);
|
||||||
|
|
||||||
|
return Render;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(NodeUserGuide);
|
export default React.memo(NodeUserGuide);
|
||||||
@@ -218,8 +223,10 @@ function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: Co
|
|||||||
|
|
||||||
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
|
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
|
||||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||||
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const workflowStartNode = nodes.find((item) => item.type === FlowNodeTypeEnum.workflowStart)!;
|
const workflowStartNode = nodeList.find(
|
||||||
|
(item) => item.flowNodeType === FlowNodeTypeEnum.workflowStart
|
||||||
|
)!;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileSelect
|
<FileSelect
|
||||||
@@ -235,7 +242,7 @@ function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: Co
|
|||||||
|
|
||||||
// Dynamic add or delete userFilesInput
|
// Dynamic add or delete userFilesInput
|
||||||
const canUploadFiles = e.canSelectFile || e.canSelectImg;
|
const canUploadFiles = e.canSelectFile || e.canSelectImg;
|
||||||
const repeatKey = workflowStartNode?.data.outputs.find(
|
const repeatKey = workflowStartNode?.outputs.find(
|
||||||
(item) => item.key === userFilesInput.key
|
(item) => item.key === userFilesInput.key
|
||||||
);
|
);
|
||||||
if (canUploadFiles) {
|
if (canUploadFiles) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
|||||||
import { useCreation, useMemoizedFn } from 'ahooks';
|
import { useCreation, useMemoizedFn } from 'ahooks';
|
||||||
import { getEditorVariables } from '../../utils';
|
import { getEditorVariables } from '../../utils';
|
||||||
import { isArray } from 'lodash';
|
import { isArray } from 'lodash';
|
||||||
|
import { WorkflowActionContext } from '../../context/workflowInitContext';
|
||||||
|
|
||||||
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||||
const { inputs = [], nodeId } = data;
|
const { inputs = [], nodeId } = data;
|
||||||
@@ -42,7 +43,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
|
|||||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
|
|
||||||
const menuList = useRef([
|
const menuList = useRef([
|
||||||
{
|
{
|
||||||
@@ -263,44 +264,48 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const Render = useMemo(() => {
|
||||||
<NodeCard selected={selected} maxW={'1000px'} {...data}>
|
return (
|
||||||
<Box px={4} pb={4}>
|
<NodeCard selected={selected} maxW={'1000px'} {...data}>
|
||||||
<>
|
<Box px={4} pb={4}>
|
||||||
{updateList.map((updateItem, index) => (
|
<>
|
||||||
<ValueRender key={index} updateItem={updateItem} index={index} />
|
{updateList.map((updateItem, index) => (
|
||||||
))}
|
<ValueRender key={index} updateItem={updateItem} index={index} />
|
||||||
</>
|
))}
|
||||||
<Flex
|
</>
|
||||||
className="nodrag"
|
<Flex
|
||||||
cursor={'default'}
|
className="nodrag"
|
||||||
alignItems={'center'}
|
cursor={'default'}
|
||||||
position={'relative'}
|
alignItems={'center'}
|
||||||
mt={4}
|
position={'relative'}
|
||||||
>
|
mt={4}
|
||||||
<Button
|
|
||||||
variant={'whiteBase'}
|
|
||||||
leftIcon={<SmallAddIcon />}
|
|
||||||
iconSpacing={1}
|
|
||||||
w={'full'}
|
|
||||||
size={'sm'}
|
|
||||||
onClick={() => {
|
|
||||||
onUpdateList([
|
|
||||||
...updateList,
|
|
||||||
{
|
|
||||||
variable: ['', ''],
|
|
||||||
value: ['', ''],
|
|
||||||
renderType: FlowNodeInputTypeEnum.input
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('common:common.Add New')}
|
<Button
|
||||||
</Button>
|
variant={'whiteBase'}
|
||||||
</Flex>
|
leftIcon={<SmallAddIcon />}
|
||||||
</Box>
|
iconSpacing={1}
|
||||||
</NodeCard>
|
w={'full'}
|
||||||
);
|
size={'sm'}
|
||||||
|
onClick={() => {
|
||||||
|
onUpdateList([
|
||||||
|
...updateList,
|
||||||
|
{
|
||||||
|
variable: ['', ''],
|
||||||
|
value: ['', ''],
|
||||||
|
renderType: FlowNodeInputTypeEnum.input
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common:common.Add New')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</NodeCard>
|
||||||
|
);
|
||||||
|
}, [ValueRender, data, onUpdateList, selected, t, updateList]);
|
||||||
|
|
||||||
|
return Render;
|
||||||
};
|
};
|
||||||
export default React.memo(NodeVariableUpdate);
|
export default React.memo(NodeVariableUpdate);
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider';
|
|||||||
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { nodeId, outputs } = data;
|
const { nodeId, outputs } = data;
|
||||||
|
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
|
||||||
|
|
||||||
const customGlobalVariables = useCreation(() => {
|
const customGlobalVariables = useCreation(() => {
|
||||||
const globalVariables = formatEditorVariablePickerIcon(
|
const globalVariables = formatEditorVariablePickerIcon(
|
||||||
@@ -62,34 +62,37 @@ const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
|||||||
})),
|
})),
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
|
const Render = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<NodeCard
|
||||||
|
minW={'420px'}
|
||||||
|
selected={selected}
|
||||||
|
menuForbid={{
|
||||||
|
copy: true,
|
||||||
|
delete: true
|
||||||
|
}}
|
||||||
|
{...data}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<IOTitle text={t('common:common.Output')} />
|
||||||
|
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||||
|
</Container>
|
||||||
|
<Container>
|
||||||
|
<IOTitle text={t('common:core.module.Variable')} />
|
||||||
|
{customGlobalVariables.length > 0 && (
|
||||||
|
<>
|
||||||
|
<RenderOutput nodeId={nodeId} flowOutputList={customGlobalVariables} />
|
||||||
|
<MyDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
return (
|
<RenderOutput nodeId={nodeId} flowOutputList={systemVariables} />
|
||||||
<NodeCard
|
</Container>
|
||||||
minW={'420px'}
|
</NodeCard>
|
||||||
selected={selected}
|
);
|
||||||
menuForbid={{
|
}, [customGlobalVariables, data, nodeId, outputs, selected, systemVariables, t]);
|
||||||
copy: true,
|
|
||||||
delete: true
|
|
||||||
}}
|
|
||||||
{...data}
|
|
||||||
>
|
|
||||||
<Container>
|
|
||||||
<IOTitle text={t('common:common.Output')} />
|
|
||||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
|
||||||
</Container>
|
|
||||||
<Container>
|
|
||||||
<IOTitle text={t('common:core.module.Variable')} />
|
|
||||||
{customGlobalVariables.length > 0 && (
|
|
||||||
<>
|
|
||||||
<RenderOutput nodeId={nodeId} flowOutputList={customGlobalVariables} />
|
|
||||||
<MyDivider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<RenderOutput nodeId={nodeId} flowOutputList={systemVariables} />
|
return Render;
|
||||||
</Container>
|
|
||||||
</NodeCard>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(NodeStart);
|
export default React.memo(NodeStart);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
|||||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../../../context';
|
import { WorkflowContext } from '../../../../context';
|
||||||
|
import { WorkflowActionContext } from '../../../../context/workflowInitContext';
|
||||||
|
|
||||||
export const ConnectionSourceHandle = ({
|
export const ConnectionSourceHandle = ({
|
||||||
nodeId,
|
nodeId,
|
||||||
@@ -13,7 +14,8 @@ export const ConnectionSourceHandle = ({
|
|||||||
nodeId: string;
|
nodeId: string;
|
||||||
isFoldNode?: boolean;
|
isFoldNode?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
|
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
||||||
|
|
||||||
const { showSourceHandle, RightHandle, LeftHandlee, TopHandlee, BottomHandlee } = useMemo(() => {
|
const { showSourceHandle, RightHandle, LeftHandlee, TopHandlee, BottomHandlee } = useMemo(() => {
|
||||||
const node = nodeList.find((node) => node.nodeId === nodeId);
|
const node = nodeList.find((node) => node.nodeId === nodeId);
|
||||||
@@ -135,7 +137,8 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
|
|||||||
}: {
|
}: {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
}) {
|
}) {
|
||||||
const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
|
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
||||||
|
|
||||||
const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
|
const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
|
||||||
const node = nodeList.find((node) => node.nodeId === nodeId);
|
const node = nodeList.find((node) => node.nodeId === nodeId);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Connection, Handle, Position } from 'reactflow';
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
||||||
|
import { WorkflowActionContext } from '../../../../context/workflowInitContext';
|
||||||
|
|
||||||
const handleSize = '16px';
|
const handleSize = '16px';
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ type ToolHandleProps = BoxProps & {
|
|||||||
export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
|
|
||||||
const handleId = NodeOutputKeyEnum.selectedTools;
|
const handleId = NodeOutputKeyEnum.selectedTools;
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
|
|||||||
|
|
||||||
export const ToolSourceHandle = () => {
|
export const ToolSourceHandle = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
|
const setEdges = useContextSelector(WorkflowActionContext, (v) => v.setEdges);
|
||||||
|
|
||||||
/* onConnect edge, delete tool input and switch */
|
/* onConnect edge, delete tool input and switch */
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
|||||||
import { useContextSelector } from 'use-context-selector';
|
import { useContextSelector } from 'use-context-selector';
|
||||||
import { WorkflowContext } from '../../../../context';
|
import { WorkflowContext } from '../../../../context';
|
||||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||||
|
import {
|
||||||
|
WorkflowActionContext,
|
||||||
|
WorkflowInitContext
|
||||||
|
} from '../../../../context/workflowInitContext';
|
||||||
|
import { WorkflowEventContext } from '../../../../context/workflowEventContext';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -24,11 +29,10 @@ const MySourceHandle = React.memo(function MySourceHandle({
|
|||||||
highlightStyle: Record<string, any>;
|
highlightStyle: Record<string, any>;
|
||||||
connectedStyle: Record<string, any>;
|
connectedStyle: Record<string, any>;
|
||||||
}) {
|
}) {
|
||||||
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
|
||||||
|
const hoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.hoverNodeId);
|
||||||
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
|
|
||||||
const hoverNodeId = useContextSelector(WorkflowContext, (v) => v.hoverNodeId);
|
|
||||||
|
|
||||||
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
|
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
|
||||||
const connected = edges.some((edge) => edge.sourceHandle === handleId);
|
const connected = edges.some((edge) => edge.sourceHandle === handleId);
|
||||||
@@ -142,7 +146,8 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
|
|||||||
highlightStyle: Record<string, any>;
|
highlightStyle: Record<string, any>;
|
||||||
connectedStyle: Record<string, any>;
|
connectedStyle: Record<string, any>;
|
||||||
}) {
|
}) {
|
||||||
const { connectingEdge, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
|
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
|
||||||
|
|
||||||
const connected = edges.some((edge) => edge.targetHandle === handleId);
|
const connected = edges.some((edge) => edge.targetHandle === handleId);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
|||||||
import { useWorkflowUtils } from '../../hooks/useUtils';
|
import { useWorkflowUtils } from '../../hooks/useUtils';
|
||||||
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
|
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
|
||||||
import { getDocPath } from '@/web/common/system/doc';
|
import { getDocPath } from '@/web/common/system/doc';
|
||||||
|
import { WorkflowActionContext } from '../../../context/workflowInitContext';
|
||||||
|
import { WorkflowEventContext } from '../../../context/workflowEventContext';
|
||||||
|
|
||||||
type Props = FlowNodeItemType & {
|
type Props = FlowNodeItemType & {
|
||||||
children?: React.ReactNode | React.ReactNode[] | string;
|
children?: React.ReactNode | React.ReactNode[] | string;
|
||||||
@@ -68,10 +70,10 @@ const NodeCard = (props: Props) => {
|
|||||||
customStyle
|
customStyle
|
||||||
} = props;
|
} = props;
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const setHoverNodeId = useContextSelector(WorkflowContext, (v) => v.setHoverNodeId);
|
|
||||||
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
||||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||||
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
|
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
|
||||||
|
const setHoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverNodeId);
|
||||||
|
|
||||||
// custom title edit
|
// custom title edit
|
||||||
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
|
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
|
||||||
@@ -391,7 +393,8 @@ const MenuRender = React.memo(function MenuRender({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { openDebugNode, DebugInputModal } = useDebug();
|
const { openDebugNode, DebugInputModal } = useDebug();
|
||||||
|
|
||||||
const { setNodes, setEdges, onNodesChange } = useContextSelector(WorkflowContext, (v) => v);
|
const setNodes = useContextSelector(WorkflowActionContext, (v) => v.setNodes);
|
||||||
|
const setEdges = useContextSelector(WorkflowActionContext, (v) => v.setEdges);
|
||||||
const { computedNewNodeName } = useWorkflowUtils();
|
const { computedNewNodeName } = useWorkflowUtils();
|
||||||
|
|
||||||
const onCopyNode = useCallback(
|
const onCopyNode = useCallback(
|
||||||
|
|||||||
@@ -42,58 +42,41 @@ const InputLabel = ({ nodeId, input }: Props) => {
|
|||||||
},
|
},
|
||||||
[input, nodeId, onChangeNode, renderTypeList]
|
[input, nodeId, onChangeNode, renderTypeList]
|
||||||
);
|
);
|
||||||
|
const renderType = renderTypeList?.[selectedTypeIndex || 0];
|
||||||
|
|
||||||
const RenderLabel = useMemo(() => {
|
return (
|
||||||
const renderType = renderTypeList?.[selectedTypeIndex || 0];
|
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
||||||
|
<Flex alignItems={'center'} position={'relative'} fontWeight={'medium'}>
|
||||||
return (
|
<FormLabel required={required} color={'myGray.600'}>
|
||||||
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
|
{t(label as any)}
|
||||||
<Flex alignItems={'center'} position={'relative'} fontWeight={'medium'}>
|
</FormLabel>
|
||||||
<FormLabel required={required} color={'myGray.600'}>
|
{description && <QuestionTip ml={1} label={t(description as any)}></QuestionTip>}
|
||||||
{t(label as any)}
|
|
||||||
</FormLabel>
|
|
||||||
{description && <QuestionTip ml={1} label={t(description as any)}></QuestionTip>}
|
|
||||||
</Flex>
|
|
||||||
{/* value type */}
|
|
||||||
{[FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.fileSelect].includes(
|
|
||||||
renderType
|
|
||||||
) && <ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />}
|
|
||||||
|
|
||||||
{/* input type select */}
|
|
||||||
{renderTypeList && renderTypeList.length > 1 && (
|
|
||||||
<Box ml={2}>
|
|
||||||
<NodeInputSelect
|
|
||||||
renderTypeList={renderTypeList}
|
|
||||||
renderTypeIndex={selectedTypeIndex}
|
|
||||||
onChange={onChangeRenderType}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Variable picker tip */}
|
|
||||||
{input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && (
|
|
||||||
<>
|
|
||||||
<Box flex={1} />
|
|
||||||
<VariableTip transform={'translateY(2px)'} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
{/* value type */}
|
||||||
}, [
|
{[FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.fileSelect].includes(renderType) && (
|
||||||
description,
|
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
|
||||||
input.renderTypeList,
|
)}
|
||||||
input.selectedTypeIndex,
|
|
||||||
label,
|
|
||||||
onChangeRenderType,
|
|
||||||
renderTypeList,
|
|
||||||
required,
|
|
||||||
selectedTypeIndex,
|
|
||||||
t,
|
|
||||||
valueDesc,
|
|
||||||
valueType
|
|
||||||
]);
|
|
||||||
|
|
||||||
return RenderLabel;
|
{/* input type select */}
|
||||||
|
{renderTypeList && renderTypeList.length > 1 && (
|
||||||
|
<Box ml={2}>
|
||||||
|
<NodeInputSelect
|
||||||
|
renderTypeList={renderTypeList}
|
||||||
|
renderTypeIndex={selectedTypeIndex}
|
||||||
|
onChange={onChangeRenderType}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Variable picker tip */}
|
||||||
|
{input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && (
|
||||||
|
<>
|
||||||
|
<Box flex={1} />
|
||||||
|
<VariableTip transform={'translateY(2px)'} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(InputLabel);
|
export default React.memo(InputLabel);
|
||||||
|
|||||||
@@ -85,54 +85,45 @@ type Props = {
|
|||||||
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
|
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
|
||||||
const { feConfigs } = useSystemStore();
|
const { feConfigs } = useSystemStore();
|
||||||
|
|
||||||
const copyInputs = useMemo(
|
|
||||||
() =>
|
|
||||||
JSON.stringify(
|
|
||||||
flowInputList.filter((input) => {
|
|
||||||
if (input.isPro && !feConfigs?.isPlus) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
),
|
|
||||||
[feConfigs?.isPlus, flowInputList]
|
|
||||||
);
|
|
||||||
const filterInputs = useMemo(() => {
|
const filterInputs = useMemo(() => {
|
||||||
return JSON.parse(copyInputs) as FlowNodeInputItemType[];
|
return flowInputList.filter((input) => {
|
||||||
}, [copyInputs]);
|
if (input.isPro && !feConfigs?.isPlus) return false;
|
||||||
|
return true;
|
||||||
const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]);
|
|
||||||
|
|
||||||
const Render = useMemo(() => {
|
|
||||||
return filterInputs.map((input) => {
|
|
||||||
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
|
|
||||||
const isDynamic = !!input.canEdit;
|
|
||||||
|
|
||||||
const RenderComponent = (() => {
|
|
||||||
if (renderType === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) {
|
|
||||||
return <>{memoCustomComponent[input.key]({ ...input })}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
|
|
||||||
|
|
||||||
if (!Component) return null;
|
|
||||||
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
|
|
||||||
})();
|
|
||||||
|
|
||||||
return renderType !== FlowNodeInputTypeEnum.hidden && !isDynamic ? (
|
|
||||||
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
|
|
||||||
{!!input.label && !hideLabelTypeList.includes(renderType) && (
|
|
||||||
<InputLabel nodeId={nodeId} input={input} />
|
|
||||||
)}
|
|
||||||
{!!RenderComponent && (
|
|
||||||
<Box mt={2} className={'nodrag'}>
|
|
||||||
{RenderComponent}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
) : null;
|
|
||||||
});
|
});
|
||||||
}, [filterInputs, mb, memoCustomComponent, nodeId]);
|
}, [feConfigs?.isPlus, flowInputList]);
|
||||||
|
|
||||||
return <>{Render}</>;
|
return (
|
||||||
|
<>
|
||||||
|
{filterInputs.map((input) => {
|
||||||
|
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
|
||||||
|
const isDynamic = !!input.canEdit;
|
||||||
|
|
||||||
|
const RenderComponent = (() => {
|
||||||
|
if (renderType === FlowNodeInputTypeEnum.custom && CustomComponent?.[input.key]) {
|
||||||
|
return <>{CustomComponent?.[input.key]({ ...input })}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
|
||||||
|
|
||||||
|
if (!Component) return null;
|
||||||
|
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return renderType !== FlowNodeInputTypeEnum.hidden && !isDynamic ? (
|
||||||
|
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
|
||||||
|
{!!input.label && !hideLabelTypeList.includes(renderType) && (
|
||||||
|
<InputLabel nodeId={nodeId} input={input} />
|
||||||
|
)}
|
||||||
|
{!!RenderComponent && (
|
||||||
|
<Box mt={2} className={'nodrag'}>
|
||||||
|
{RenderComponent}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(RenderInput);
|
export default React.memo(RenderInput);
|
||||||
|
|||||||
@@ -50,25 +50,21 @@ const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
|||||||
}
|
}
|
||||||
return JSON.stringify(item.value, null, 2);
|
return JSON.stringify(item.value, null, 2);
|
||||||
}, [item.value]);
|
}, [item.value]);
|
||||||
|
console.log(12121);
|
||||||
const Render = useMemo(() => {
|
return (
|
||||||
return (
|
<JSONEditor
|
||||||
<JSONEditor
|
className="nowheel"
|
||||||
className="nowheel"
|
bg={'white'}
|
||||||
bg={'white'}
|
borderRadius={'sm'}
|
||||||
borderRadius={'sm'}
|
placeholder={t(item.placeholder as any)}
|
||||||
placeholder={t(item.placeholder as any)}
|
resize
|
||||||
resize
|
value={value}
|
||||||
value={value}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
update(e);
|
||||||
update(e);
|
}}
|
||||||
}}
|
variables={variables}
|
||||||
variables={variables}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
}, [item.placeholder, t, update, value, variables]);
|
|
||||||
|
|
||||||
return Render;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(JsonEditor);
|
export default React.memo(JsonEditor);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useContextSelector } from 'use-context-selector';
|
|||||||
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
||||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
import { AppContext } from '@/pages/app/detail/components/context';
|
import { AppContext } from '@/pages/app/detail/components/context';
|
||||||
|
import { WorkflowActionContext } from '../../../../../context/workflowInitContext';
|
||||||
|
|
||||||
const MultipleRowSelect = dynamic(() =>
|
const MultipleRowSelect = dynamic(() =>
|
||||||
import('@fastgpt/web/components/common/MySelect/MultipleRowSelect').then(
|
import('@fastgpt/web/components/common/MySelect/MultipleRowSelect').then(
|
||||||
@@ -59,8 +60,9 @@ export const useReference = ({
|
|||||||
valueType?: WorkflowIOValueTypeEnum;
|
valueType?: WorkflowIOValueTypeEnum;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||||
const { nodeList, edges } = useContextSelector(WorkflowContext, (v) => v);
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
|
|
||||||
// 获取可选的变量列表
|
// 获取可选的变量列表
|
||||||
const referenceList = useMemo(() => {
|
const referenceList = useMemo(() => {
|
||||||
@@ -319,7 +321,7 @@ const MultipleReferenceSelector = ({
|
|||||||
popDirection={popDirection}
|
popDirection={popDirection}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [getSelectValue, list, onSelect, placeholder, popDirection, t, value]);
|
}, [getSelectValue, list, onSelect, placeholder, popDirection, value]);
|
||||||
|
|
||||||
return ArraySelector;
|
return ArraySelector;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent
|
|||||||
import { useCreation } from 'ahooks';
|
import { useCreation } from 'ahooks';
|
||||||
import { AppContext } from '@/pages/app/detail/components/context';
|
import { AppContext } from '@/pages/app/detail/components/context';
|
||||||
import { getEditorVariables } from '../../../../../utils';
|
import { getEditorVariables } from '../../../../../utils';
|
||||||
|
import { WorkflowActionContext } from '../../../../../context/workflowInitContext';
|
||||||
|
|
||||||
const TextInputRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
const TextInputRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { nodeList, edges, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
|
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||||
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
|
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||||
|
|
||||||
// get variable
|
// get variable
|
||||||
const variables = useCreation(() => {
|
const variables = useCreation(() => {
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent
|
|||||||
import { useCreation } from 'ahooks';
|
import { useCreation } from 'ahooks';
|
||||||
import { AppContext } from '@/pages/app/detail/components/context';
|
import { AppContext } from '@/pages/app/detail/components/context';
|
||||||
import { getEditorVariables } from '../../../../../utils';
|
import { getEditorVariables } from '../../../../../utils';
|
||||||
|
import { WorkflowActionContext } from '../../../../../context/workflowInitContext';
|
||||||
|
|
||||||
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const edges = useContextSelector(WorkflowActionContext, (v) => v.edges);
|
||||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
|
||||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||||
|
|
||||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||||
|
|||||||
@@ -13,44 +13,40 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { label = '', description, valueType, valueDesc } = output;
|
const { label = '', description, valueType, valueDesc } = output;
|
||||||
|
|
||||||
const Render = useMemo(() => {
|
return (
|
||||||
return (
|
<Box position={'relative'}>
|
||||||
<Box position={'relative'}>
|
<Flex
|
||||||
<Flex
|
className="nodrag"
|
||||||
className="nodrag"
|
cursor={'default'}
|
||||||
cursor={'default'}
|
alignItems={'center'}
|
||||||
alignItems={'center'}
|
fontWeight={'medium'}
|
||||||
fontWeight={'medium'}
|
color={'myGray.600'}
|
||||||
color={'myGray.600'}
|
{...(output.type === FlowNodeOutputTypeEnum.source
|
||||||
{...(output.type === FlowNodeOutputTypeEnum.source
|
? {
|
||||||
? {
|
flexDirection: 'row-reverse'
|
||||||
flexDirection: 'row-reverse'
|
}
|
||||||
}
|
: {})}
|
||||||
: {})}
|
>
|
||||||
|
<Box
|
||||||
|
position={'relative'}
|
||||||
|
mr={1}
|
||||||
|
ml={output.type === FlowNodeOutputTypeEnum.source ? 1 : 0}
|
||||||
>
|
>
|
||||||
<Box
|
{t(label as any)}
|
||||||
position={'relative'}
|
</Box>
|
||||||
mr={1}
|
{description && <QuestionTip ml={1} label={t(description as any)} />}
|
||||||
ml={output.type === FlowNodeOutputTypeEnum.source ? 1 : 0}
|
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
|
||||||
>
|
</Flex>
|
||||||
{t(label as any)}
|
{output.type === FlowNodeOutputTypeEnum.source && (
|
||||||
</Box>
|
<SourceHandle
|
||||||
{description && <QuestionTip ml={1} label={t(description as any)} />}
|
nodeId={nodeId}
|
||||||
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
|
handleId={getHandleId(nodeId, 'source', output.key)}
|
||||||
</Flex>
|
translate={[26, 0]}
|
||||||
{output.type === FlowNodeOutputTypeEnum.source && (
|
position={Position.Right}
|
||||||
<SourceHandle
|
/>
|
||||||
nodeId={nodeId}
|
)}
|
||||||
handleId={getHandleId(nodeId, 'source', output.key)}
|
</Box>
|
||||||
translate={[26, 0]}
|
);
|
||||||
position={Position.Right}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}, [output.type, output.key, t, label, description, valueType, valueDesc, nodeId]);
|
|
||||||
|
|
||||||
return Render;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(OutputLabel);
|
export default React.memo(OutputLabel);
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DefaultEdgeOptions } from 'reactflow';
|
import { DefaultEdgeOptions } from 'reactflow';
|
||||||
|
|
||||||
|
export const minZoom = 0.1;
|
||||||
|
export const maxZoom = 1.5;
|
||||||
|
|
||||||
export const connectionLineStyle: React.CSSProperties = {
|
export const connectionLineStyle: React.CSSProperties = {
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
stroke: '#487FFF'
|
stroke: '#487FFF'
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor
|
|||||||
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
|
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
|
||||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||||
import { useDebounceEffect, useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
|
import { useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
|
||||||
import React, {
|
import React, {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
@@ -25,31 +25,40 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import { Edge, Node, OnConnectStartParams, ReactFlowProvider, useReactFlow } from 'reactflow';
|
||||||
Edge,
|
|
||||||
EdgeChange,
|
|
||||||
Node,
|
|
||||||
NodeChange,
|
|
||||||
OnConnectStartParams,
|
|
||||||
useEdgesState,
|
|
||||||
useNodesState,
|
|
||||||
useReactFlow
|
|
||||||
} from 'reactflow';
|
|
||||||
import { createContext, useContextSelector } from 'use-context-selector';
|
import { createContext, useContextSelector } from 'use-context-selector';
|
||||||
import { defaultRunningStatus } from './constants';
|
import { defaultRunningStatus } from '../constants';
|
||||||
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
|
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
|
||||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||||
import { AppContext } from '@/pages/app/detail/components/context';
|
import { AppContext } from '@/pages/app/detail/components/context';
|
||||||
import ChatTest from './Flow/ChatTest';
|
import ChatTest from '../Flow/ChatTest';
|
||||||
import { useDisclosure } from '@chakra-ui/react';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
import { uiWorkflow2StoreWorkflow } from './utils';
|
import { uiWorkflow2StoreWorkflow } from '../utils';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { SetState } from 'ahooks/lib/createUseStorageState';
|
|
||||||
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||||
|
import WorkflowInitContextProvider, { WorkflowActionContext } from './workflowInitContext';
|
||||||
|
import WorkflowEventContextProvider from './workflowEventContext';
|
||||||
|
|
||||||
|
export const ReactFlowCustomProvider = ({
|
||||||
|
templates,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
templates: FlowNodeTemplateType[];
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<WorkflowInitContextProvider>
|
||||||
|
<WorkflowContextProvider basicNodeTemplates={templates}>
|
||||||
|
<WorkflowEventContextProvider>{children}</WorkflowEventContextProvider>
|
||||||
|
</WorkflowContextProvider>
|
||||||
|
</WorkflowInitContextProvider>
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||||
|
|
||||||
@@ -65,33 +74,22 @@ type WorkflowContextType = {
|
|||||||
appId?: string;
|
appId?: string;
|
||||||
basicNodeTemplates: FlowNodeTemplateType[];
|
basicNodeTemplates: FlowNodeTemplateType[];
|
||||||
filterAppIds?: string[];
|
filterAppIds?: string[];
|
||||||
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
|
|
||||||
mouseInCanvas: boolean;
|
|
||||||
|
|
||||||
// nodes
|
// nodes
|
||||||
nodes: Node<FlowNodeItemType, string | undefined>[];
|
|
||||||
nodeList: FlowNodeItemType[];
|
nodeList: FlowNodeItemType[];
|
||||||
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
|
|
||||||
onNodesChange: OnChange<NodeChange>;
|
|
||||||
hasToolNode: boolean;
|
hasToolNode: boolean;
|
||||||
hoverNodeId?: string;
|
|
||||||
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
||||||
onUpdateNodeError: (node: string, isError: Boolean) => void;
|
onUpdateNodeError: (node: string, isError: Boolean) => void;
|
||||||
onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void;
|
onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void;
|
||||||
onChangeNode: (e: FlowNodeChangeProps) => void;
|
onChangeNode: (e: FlowNodeChangeProps) => void;
|
||||||
getNodeDynamicInputs: (nodeId: string) => FlowNodeInputItemType[];
|
getNodeDynamicInputs: (nodeId: string) => FlowNodeInputItemType[];
|
||||||
|
|
||||||
// edges
|
// edges
|
||||||
edges: Edge<any>[];
|
|
||||||
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
|
|
||||||
onEdgesChange: OnChange<EdgeChange>;
|
|
||||||
onDelEdge: (e: {
|
onDelEdge: (e: {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
sourceHandle?: string | undefined;
|
sourceHandle?: string | undefined;
|
||||||
targetHandle?: string | undefined;
|
targetHandle?: string | undefined;
|
||||||
}) => void;
|
}) => void;
|
||||||
hoverEdgeId?: string;
|
|
||||||
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
||||||
|
|
||||||
onSwitchTmpVersion: (data: WorkflowSnapshotsType, customTitle: string) => boolean;
|
onSwitchTmpVersion: (data: WorkflowSnapshotsType, customTitle: string) => boolean;
|
||||||
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => boolean;
|
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => boolean;
|
||||||
@@ -102,6 +100,19 @@ type WorkflowContextType = {
|
|||||||
undo: () => void;
|
undo: () => void;
|
||||||
canRedo: boolean;
|
canRedo: boolean;
|
||||||
canUndo: boolean;
|
canUndo: boolean;
|
||||||
|
pushPastSnapshot: ({
|
||||||
|
pastNodes,
|
||||||
|
pastEdges,
|
||||||
|
customTitle,
|
||||||
|
chatConfig,
|
||||||
|
isSaved
|
||||||
|
}: {
|
||||||
|
pastNodes: Node[];
|
||||||
|
pastEdges: Edge[];
|
||||||
|
customTitle?: string;
|
||||||
|
chatConfig: AppChatConfigType;
|
||||||
|
isSaved?: boolean;
|
||||||
|
}) => boolean;
|
||||||
|
|
||||||
// connect
|
// connect
|
||||||
connectingEdge?: OnConnectStartParams;
|
connectingEdge?: OnConnectStartParams;
|
||||||
@@ -159,10 +170,6 @@ type WorkflowContextType = {
|
|||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
onStopNodeDebug: () => void;
|
onStopNodeDebug: () => void;
|
||||||
|
|
||||||
// version history
|
|
||||||
showHistoryModal: boolean;
|
|
||||||
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
|
|
||||||
// chat test
|
// chat test
|
||||||
setWorkflowTestData: React.Dispatch<
|
setWorkflowTestData: React.Dispatch<
|
||||||
React.SetStateAction<
|
React.SetStateAction<
|
||||||
@@ -173,15 +180,6 @@ type WorkflowContextType = {
|
|||||||
| undefined
|
| undefined
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
//
|
|
||||||
workflowControlMode?: 'drag' | 'select';
|
|
||||||
setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void;
|
|
||||||
menu: {
|
|
||||||
top: number;
|
|
||||||
left: number;
|
|
||||||
} | null;
|
|
||||||
setMenu: (value: React.SetStateAction<{ top: number; left: number } | null>) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type DebugDataType = {
|
type DebugDataType = {
|
||||||
@@ -198,32 +196,11 @@ export const WorkflowContext = createContext<WorkflowContextType>({
|
|||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
basicNodeTemplates: [],
|
basicNodeTemplates: [],
|
||||||
reactFlowWrapper: null,
|
|
||||||
nodes: [],
|
|
||||||
nodeList: [],
|
nodeList: [],
|
||||||
mouseInCanvas: false,
|
|
||||||
setNodes: function (
|
|
||||||
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
|
|
||||||
): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
onNodesChange: function (changes: NodeChange[]): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
hasToolNode: false,
|
hasToolNode: false,
|
||||||
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
onUpdateNodeError: function (node: string, isError: Boolean): void {
|
onUpdateNodeError: function (node: string, isError: Boolean): void {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
edges: [],
|
|
||||||
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
onEdgesChange: function (changes: EdgeChange[]): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void {
|
onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
@@ -272,9 +249,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
|
|||||||
onChangeNode: function (e: FlowNodeChangeProps): void {
|
onChangeNode: function (e: FlowNodeChangeProps): void {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
setHoverEdgeId: function (value: React.SetStateAction<string | undefined>): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
setWorkflowTestData: function (
|
setWorkflowTestData: function (
|
||||||
value: React.SetStateAction<
|
value: React.SetStateAction<
|
||||||
{ nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] } | undefined
|
{ nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] } | undefined
|
||||||
@@ -292,10 +266,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
|
|||||||
| undefined {
|
| undefined {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
showHistoryModal: false,
|
|
||||||
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
|
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
@@ -312,18 +282,27 @@ export const WorkflowContext = createContext<WorkflowContextType>({
|
|||||||
},
|
},
|
||||||
canRedo: false,
|
canRedo: false,
|
||||||
canUndo: false,
|
canUndo: false,
|
||||||
workflowControlMode: 'drag',
|
|
||||||
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
},
|
|
||||||
onSwitchTmpVersion: function (data: WorkflowSnapshotsType, customTitle: string): boolean {
|
onSwitchTmpVersion: function (data: WorkflowSnapshotsType, customTitle: string): boolean {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean {
|
onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
},
|
},
|
||||||
menu: null,
|
|
||||||
setMenu: function (value: React.SetStateAction<{ top: number; left: number } | null>): void {
|
pushPastSnapshot: function ({
|
||||||
|
pastNodes,
|
||||||
|
pastEdges,
|
||||||
|
customTitle,
|
||||||
|
chatConfig,
|
||||||
|
isSaved
|
||||||
|
}: {
|
||||||
|
pastNodes: Node[];
|
||||||
|
pastEdges: Edge[];
|
||||||
|
customTitle?: string;
|
||||||
|
chatConfig: AppChatConfigType;
|
||||||
|
isSaved?: boolean;
|
||||||
|
}): boolean {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -337,39 +316,14 @@ const WorkflowContextProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
|
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||||
|
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
|
||||||
const appId = appDetail._id;
|
const appId = appDetail._id;
|
||||||
|
|
||||||
const [workflowControlMode, setWorkflowControlMode] = useLocalStorageState<'drag' | 'select'>(
|
|
||||||
'workflow-control-mode',
|
|
||||||
{
|
|
||||||
defaultValue: 'drag',
|
|
||||||
listenStorageChange: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mouse in canvas
|
|
||||||
const [mouseInCanvas, setMouseInCanvas] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseInCanvas = (e: MouseEvent) => {
|
|
||||||
setMouseInCanvas(true);
|
|
||||||
};
|
|
||||||
const handleMouseOutCanvas = (e: MouseEvent) => {
|
|
||||||
setMouseInCanvas(false);
|
|
||||||
};
|
|
||||||
reactFlowWrapper?.current?.addEventListener('mouseenter', handleMouseInCanvas);
|
|
||||||
reactFlowWrapper?.current?.addEventListener('mouseleave', handleMouseOutCanvas);
|
|
||||||
return () => {
|
|
||||||
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
|
|
||||||
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
|
|
||||||
};
|
|
||||||
}, [reactFlowWrapper?.current]);
|
|
||||||
|
|
||||||
/* edge */
|
/* edge */
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const edges = useContextSelector(WorkflowActionContext, (state) => state.edges);
|
||||||
const [hoverEdgeId, setHoverEdgeId] = useState<string>();
|
const setEdges = useContextSelector(WorkflowActionContext, (state) => state.setEdges);
|
||||||
const onDelEdge = useCallback(
|
const onDelEdge = useCallback(
|
||||||
({
|
({
|
||||||
nodeId,
|
nodeId,
|
||||||
@@ -397,32 +351,16 @@ const WorkflowContextProvider = ({
|
|||||||
const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>();
|
const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>();
|
||||||
|
|
||||||
/* node */
|
/* node */
|
||||||
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
|
const setNodes = useContextSelector(WorkflowActionContext, (state) => state.setNodes);
|
||||||
const [hoverNodeId, setHoverNodeId] = useState<string>();
|
const getNodes = useContextSelector(WorkflowActionContext, (state) => state.getNodes);
|
||||||
|
const nodeListString = useContextSelector(WorkflowActionContext, (state) => state.nodeListString);
|
||||||
|
|
||||||
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
|
console.log(121211111111);
|
||||||
const nodeList = useMemo(
|
const nodeList = useMemo(
|
||||||
() => JSON.parse(nodeListString) as FlowNodeItemType[],
|
() => JSON.parse(nodeListString) as FlowNodeItemType[],
|
||||||
[nodeListString]
|
[nodeListString]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Elevate childNodes
|
|
||||||
useEffect(() => {
|
|
||||||
setNodes((nodes) =>
|
|
||||||
nodes.map((node) => (node.data.parentNodeId ? { ...node, zIndex: 1001 } : node))
|
|
||||||
);
|
|
||||||
}, [nodeList]);
|
|
||||||
// Elevate edges of childNodes
|
|
||||||
useEffect(() => {
|
|
||||||
setEdges((state) =>
|
|
||||||
state.map((item) =>
|
|
||||||
nodeList.some((node) => item.source === node.nodeId && node.parentNodeId)
|
|
||||||
? { ...item, zIndex: 1001 }
|
|
||||||
: item
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, [edges.length]);
|
|
||||||
|
|
||||||
const hasToolNode = useMemo(() => {
|
const hasToolNode = useMemo(() => {
|
||||||
return !!nodeList.find((node) => node.flowNodeType === FlowNodeTypeEnum.tools);
|
return !!nodeList.find((node) => node.flowNodeType === FlowNodeTypeEnum.tools);
|
||||||
}, [nodeList]);
|
}, [nodeList]);
|
||||||
@@ -571,6 +509,7 @@ const WorkflowContextProvider = ({
|
|||||||
/* ui flow to store data */
|
/* ui flow to store data */
|
||||||
const { fitView } = useReactFlow();
|
const { fitView } = useReactFlow();
|
||||||
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
|
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
|
||||||
|
const nodes = getNodes();
|
||||||
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||||
|
|
||||||
if (!checkResults) {
|
if (!checkResults) {
|
||||||
@@ -593,6 +532,7 @@ const WorkflowContextProvider = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const flowData2StoreData = useMemoizedFn(() => {
|
const flowData2StoreData = useMemoizedFn(() => {
|
||||||
|
const nodes = getNodes();
|
||||||
return uiWorkflow2StoreWorkflow({ nodes, edges });
|
return uiWorkflow2StoreWorkflow({ nodes, edges });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -892,22 +832,6 @@ const WorkflowContextProvider = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto save snapshot
|
|
||||||
useDebounceEffect(
|
|
||||||
() => {
|
|
||||||
if (nodes.length === 0 || !appDetail.chatConfig) return;
|
|
||||||
|
|
||||||
pushPastSnapshot({
|
|
||||||
pastNodes: nodes,
|
|
||||||
pastEdges: edges,
|
|
||||||
customTitle: formatTime2YMDHMS(new Date()),
|
|
||||||
chatConfig: appDetail.chatConfig
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[nodes, edges, appDetail.chatConfig],
|
|
||||||
{ wait: 500 }
|
|
||||||
);
|
|
||||||
|
|
||||||
const undo = useMemoizedFn(() => {
|
const undo = useMemoizedFn(() => {
|
||||||
if (past[1]) {
|
if (past[1]) {
|
||||||
setFuture((future) => [past[0], ...future]);
|
setFuture((future) => [past[0], ...future]);
|
||||||
@@ -978,88 +902,80 @@ const WorkflowContextProvider = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Version histories */
|
const value = useMemo(
|
||||||
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
() => ({
|
||||||
|
appId,
|
||||||
|
basicNodeTemplates,
|
||||||
|
|
||||||
/* event bus */
|
// node
|
||||||
useEffect(() => {
|
nodeList,
|
||||||
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
|
hasToolNode,
|
||||||
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
|
onUpdateNodeError,
|
||||||
nodes,
|
onResetNode,
|
||||||
edges
|
onChangeNode,
|
||||||
});
|
getNodeDynamicInputs,
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
eventBus.off(EventNameEnum.requestWorkflowStore);
|
|
||||||
};
|
|
||||||
}, [edges, nodes]);
|
|
||||||
|
|
||||||
const [menu, setMenu] = useState<{ top: number; left: number } | null>(null);
|
// edge
|
||||||
|
connectingEdge,
|
||||||
|
setConnectingEdge,
|
||||||
|
onDelEdge,
|
||||||
|
|
||||||
const value = {
|
// snapshots
|
||||||
appId,
|
past,
|
||||||
reactFlowWrapper,
|
setPast,
|
||||||
basicNodeTemplates,
|
future,
|
||||||
workflowControlMode,
|
undo,
|
||||||
setWorkflowControlMode,
|
redo,
|
||||||
mouseInCanvas,
|
canUndo: past.length > 1,
|
||||||
|
canRedo: !!future.length,
|
||||||
|
onSwitchTmpVersion,
|
||||||
|
onSwitchCloudVersion,
|
||||||
|
pushPastSnapshot,
|
||||||
|
|
||||||
// node
|
// function
|
||||||
nodes,
|
splitToolInputs,
|
||||||
setNodes,
|
initData,
|
||||||
onNodesChange,
|
flowData2StoreDataAndCheck,
|
||||||
nodeList,
|
flowData2StoreData,
|
||||||
hasToolNode,
|
|
||||||
hoverNodeId,
|
|
||||||
setHoverNodeId,
|
|
||||||
onUpdateNodeError,
|
|
||||||
onResetNode,
|
|
||||||
onChangeNode,
|
|
||||||
getNodeDynamicInputs,
|
|
||||||
|
|
||||||
// edge
|
// debug
|
||||||
edges,
|
workflowDebugData,
|
||||||
setEdges,
|
onNextNodeDebug,
|
||||||
hoverEdgeId,
|
onStartNodeDebug,
|
||||||
setHoverEdgeId,
|
onStopNodeDebug,
|
||||||
onEdgesChange,
|
|
||||||
connectingEdge,
|
|
||||||
setConnectingEdge,
|
|
||||||
onDelEdge,
|
|
||||||
|
|
||||||
// snapshots
|
// chat test
|
||||||
past,
|
setWorkflowTestData
|
||||||
setPast,
|
}),
|
||||||
future,
|
[
|
||||||
undo,
|
appId,
|
||||||
redo,
|
basicNodeTemplates,
|
||||||
canUndo: past.length > 1,
|
connectingEdge,
|
||||||
canRedo: !!future.length,
|
flowData2StoreData,
|
||||||
onSwitchTmpVersion,
|
flowData2StoreDataAndCheck,
|
||||||
onSwitchCloudVersion,
|
future,
|
||||||
|
getNodeDynamicInputs,
|
||||||
// function
|
hasToolNode,
|
||||||
splitToolInputs,
|
initData,
|
||||||
initData,
|
nodeList,
|
||||||
flowData2StoreDataAndCheck,
|
onChangeNode,
|
||||||
flowData2StoreData,
|
onDelEdge,
|
||||||
|
onNextNodeDebug,
|
||||||
// debug
|
onResetNode,
|
||||||
workflowDebugData,
|
onStartNodeDebug,
|
||||||
onNextNodeDebug,
|
onStopNodeDebug,
|
||||||
onStartNodeDebug,
|
onSwitchCloudVersion,
|
||||||
onStopNodeDebug,
|
onSwitchTmpVersion,
|
||||||
|
onUpdateNodeError,
|
||||||
// version history
|
past,
|
||||||
showHistoryModal,
|
pushPastSnapshot,
|
||||||
setShowHistoryModal,
|
redo,
|
||||||
|
setPast,
|
||||||
// chat test
|
splitToolInputs,
|
||||||
setWorkflowTestData,
|
undo,
|
||||||
|
workflowDebugData
|
||||||
menu,
|
]
|
||||||
setMenu
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowContext.Provider value={value}>
|
<WorkflowContext.Provider value={value}>
|
||||||
@@ -1068,18 +984,4 @@ const WorkflowContextProvider = ({
|
|||||||
</WorkflowContext.Provider>
|
</WorkflowContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export default React.memo(WorkflowContextProvider);
|
||||||
export default WorkflowContextProvider;
|
|
||||||
|
|
||||||
type GetWorkflowStoreResponse = {
|
|
||||||
nodes: Node<FlowNodeItemType>[];
|
|
||||||
edges: Edge<any>[];
|
|
||||||
};
|
|
||||||
export const getWorkflowStore = () =>
|
|
||||||
new Promise<GetWorkflowStoreResponse>((resolve) => {
|
|
||||||
eventBus.on(EventNameEnum.receiveWorkflowStore, (data: GetWorkflowStoreResponse) => {
|
|
||||||
resolve(data);
|
|
||||||
eventBus.off(EventNameEnum.receiveWorkflowStore);
|
|
||||||
});
|
|
||||||
eventBus.emit(EventNameEnum.requestWorkflowStore);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { createContext } from 'use-context-selector';
|
||||||
|
import { useLocalStorageState } from 'ahooks';
|
||||||
|
import { SetState } from 'ahooks/lib/createUseStorageState';
|
||||||
|
|
||||||
|
type WorkflowEventContextType = {
|
||||||
|
mouseInCanvas: boolean;
|
||||||
|
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
|
||||||
|
hoverNodeId?: string;
|
||||||
|
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
hoverEdgeId?: string;
|
||||||
|
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
workflowControlMode?: 'drag' | 'select';
|
||||||
|
setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void;
|
||||||
|
menu: {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
} | null;
|
||||||
|
setMenu: (value: React.SetStateAction<{ top: number; left: number } | null>) => void;
|
||||||
|
// version history
|
||||||
|
showHistoryModal: boolean;
|
||||||
|
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowEventContext = createContext<WorkflowEventContextType>({
|
||||||
|
mouseInCanvas: false,
|
||||||
|
reactFlowWrapper: null,
|
||||||
|
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
setHoverEdgeId: function (value: React.SetStateAction<string | undefined>): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
workflowControlMode: 'drag',
|
||||||
|
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
menu: null,
|
||||||
|
setMenu: function (value: React.SetStateAction<{ top: number; left: number } | null>): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
showHistoryModal: false,
|
||||||
|
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
// Watch mouse in canvas
|
||||||
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
const [mouseInCanvas, setMouseInCanvas] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseInCanvas = (e: MouseEvent) => {
|
||||||
|
setMouseInCanvas(true);
|
||||||
|
};
|
||||||
|
const handleMouseOutCanvas = (e: MouseEvent) => {
|
||||||
|
setMouseInCanvas(false);
|
||||||
|
};
|
||||||
|
reactFlowWrapper?.current?.addEventListener('mouseenter', handleMouseInCanvas);
|
||||||
|
reactFlowWrapper?.current?.addEventListener('mouseleave', handleMouseOutCanvas);
|
||||||
|
return () => {
|
||||||
|
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
|
||||||
|
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
|
||||||
|
};
|
||||||
|
}, [reactFlowWrapper?.current, setMouseInCanvas]);
|
||||||
|
|
||||||
|
// Watch hover node
|
||||||
|
const [hoverNodeId, setHoverNodeId] = useState<string>();
|
||||||
|
// Watch hover edge
|
||||||
|
const [hoverEdgeId, setHoverEdgeId] = useState<string>();
|
||||||
|
|
||||||
|
const [workflowControlMode, setWorkflowControlMode] = useLocalStorageState<'drag' | 'select'>(
|
||||||
|
'workflow-control-mode',
|
||||||
|
{
|
||||||
|
defaultValue: 'drag',
|
||||||
|
listenStorageChange: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [menu, setMenu] = useState<{ top: number; left: number } | null>(null);
|
||||||
|
|
||||||
|
/* Version histories */
|
||||||
|
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
mouseInCanvas,
|
||||||
|
reactFlowWrapper,
|
||||||
|
hoverNodeId,
|
||||||
|
setHoverNodeId,
|
||||||
|
hoverEdgeId,
|
||||||
|
setHoverEdgeId,
|
||||||
|
workflowControlMode,
|
||||||
|
setWorkflowControlMode,
|
||||||
|
menu,
|
||||||
|
setMenu,
|
||||||
|
showHistoryModal,
|
||||||
|
setShowHistoryModal
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
mouseInCanvas,
|
||||||
|
hoverNodeId,
|
||||||
|
setHoverNodeId,
|
||||||
|
hoverEdgeId,
|
||||||
|
setHoverEdgeId,
|
||||||
|
workflowControlMode,
|
||||||
|
setWorkflowControlMode,
|
||||||
|
menu,
|
||||||
|
setMenu,
|
||||||
|
showHistoryModal,
|
||||||
|
setShowHistoryModal
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<WorkflowEventContext.Provider value={contextValue}>{children}</WorkflowEventContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkflowEventContextProvider;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { createContext } from 'use-context-selector';
|
||||||
|
import { postWorkflowDebug } from '@/web/core/workflow/api';
|
||||||
|
import {
|
||||||
|
checkWorkflowNodeAndConnection,
|
||||||
|
compareSnapshot,
|
||||||
|
storeEdgesRenderEdge,
|
||||||
|
storeNode2FlowNode
|
||||||
|
} from '@/web/core/workflow/utils';
|
||||||
|
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||||
|
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||||
|
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
|
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||||
|
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||||
|
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
|
||||||
|
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||||
|
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
|
||||||
|
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||||
|
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||||
|
import { useDebounceEffect, useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
|
||||||
|
import React, {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Edge,
|
||||||
|
EdgeChange,
|
||||||
|
Node,
|
||||||
|
NodeChange,
|
||||||
|
OnConnectStartParams,
|
||||||
|
useEdgesState,
|
||||||
|
useNodesState,
|
||||||
|
useReactFlow
|
||||||
|
} from 'reactflow';
|
||||||
|
|
||||||
|
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||||
|
|
||||||
|
type WorkflowInitContextType = {
|
||||||
|
nodes: Node<FlowNodeItemType, string | undefined>[];
|
||||||
|
};
|
||||||
|
export const WorkflowInitContext = createContext<WorkflowInitContextType>({
|
||||||
|
nodes: []
|
||||||
|
});
|
||||||
|
|
||||||
|
type WorkflowActionContextType = {
|
||||||
|
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
|
||||||
|
onNodesChange: OnChange<NodeChange>;
|
||||||
|
getNodes: () => Node<FlowNodeItemType, string | undefined>[];
|
||||||
|
nodeListString: string;
|
||||||
|
edges: Edge<any>[];
|
||||||
|
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
|
||||||
|
onEdgesChange: OnChange<EdgeChange>;
|
||||||
|
};
|
||||||
|
export const WorkflowActionContext = createContext<WorkflowActionContextType>({
|
||||||
|
setNodes: function (
|
||||||
|
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
|
||||||
|
): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
onNodesChange: function (changes: NodeChange[]): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
getNodes: function (): Node<FlowNodeItemType, string | undefined>[] {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
nodeListString: JSON.stringify([]),
|
||||||
|
edges: [],
|
||||||
|
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
onEdgesChange: function (changes: EdgeChange[]): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const WorkflowInitContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
// Nodes
|
||||||
|
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
|
||||||
|
const getNodes = useMemoizedFn(() => nodes);
|
||||||
|
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
|
||||||
|
const nodeList = useMemo(
|
||||||
|
() => JSON.parse(nodeListString) as FlowNodeItemType[],
|
||||||
|
[nodeListString]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Edges
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
|
|
||||||
|
// Elevate childNodes
|
||||||
|
useEffect(() => {
|
||||||
|
setNodes((nodes) =>
|
||||||
|
nodes.map((node) => (node.data.parentNodeId ? { ...node, zIndex: 1001 } : node))
|
||||||
|
);
|
||||||
|
}, [nodeList]);
|
||||||
|
// Elevate edges of childNodes
|
||||||
|
useEffect(() => {
|
||||||
|
setEdges((state) =>
|
||||||
|
state.map((item) =>
|
||||||
|
nodeList.some((node) => item.source === node.nodeId && node.parentNodeId)
|
||||||
|
? { ...item, zIndex: 1001 }
|
||||||
|
: item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [edges.length]);
|
||||||
|
|
||||||
|
const actionContextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
setNodes,
|
||||||
|
onNodesChange,
|
||||||
|
getNodes,
|
||||||
|
nodeListString,
|
||||||
|
|
||||||
|
edges,
|
||||||
|
setEdges,
|
||||||
|
onEdgesChange
|
||||||
|
}),
|
||||||
|
[setNodes, onNodesChange, getNodes, nodeListString, edges, setEdges, onEdgesChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowInitContext.Provider
|
||||||
|
value={{
|
||||||
|
nodes
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WorkflowActionContext.Provider value={actionContextValue}>
|
||||||
|
{children}
|
||||||
|
</WorkflowActionContext.Provider>
|
||||||
|
</WorkflowInitContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkflowInitContextProvider;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react';
|
import { Dispatch, ReactNode, SetStateAction, useCallback, useMemo, useState } from 'react';
|
||||||
import { createContext } from 'use-context-selector';
|
import { createContext } from 'use-context-selector';
|
||||||
import { defaultApp } from '@/web/core/app/constants';
|
import { defaultApp } from '@/web/core/app/constants';
|
||||||
import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api';
|
import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api';
|
||||||
@@ -186,22 +186,39 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
[appDetail.name, deleteApp, openConfirmDel, t]
|
[appDetail.name, deleteApp, openConfirmDel, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const contextValue: AppContextType = {
|
const contextValue: AppContextType = useMemo(
|
||||||
appId,
|
() => ({
|
||||||
currentTab,
|
appId,
|
||||||
route2Tab,
|
currentTab,
|
||||||
appDetail,
|
route2Tab,
|
||||||
setAppDetail,
|
appDetail,
|
||||||
loadingApp,
|
setAppDetail,
|
||||||
updateAppDetail,
|
loadingApp,
|
||||||
onOpenInfoEdit,
|
updateAppDetail,
|
||||||
onOpenTeamTagModal,
|
onOpenInfoEdit,
|
||||||
onDelApp,
|
onOpenTeamTagModal,
|
||||||
onSaveApp,
|
onDelApp,
|
||||||
appLatestVersion,
|
onSaveApp,
|
||||||
reloadAppLatestVersion,
|
appLatestVersion,
|
||||||
reloadApp
|
reloadAppLatestVersion,
|
||||||
};
|
reloadApp
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
appDetail,
|
||||||
|
appId,
|
||||||
|
appLatestVersion,
|
||||||
|
currentTab,
|
||||||
|
loadingApp,
|
||||||
|
onDelApp,
|
||||||
|
onOpenInfoEdit,
|
||||||
|
onOpenTeamTagModal,
|
||||||
|
onSaveApp,
|
||||||
|
reloadApp,
|
||||||
|
reloadAppLatestVersion,
|
||||||
|
route2Tab,
|
||||||
|
updateAppDetail
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={contextValue}>
|
<AppContext.Provider value={contextValue}>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
export enum EventNameEnum {
|
export enum EventNameEnum {
|
||||||
sendQuestion = 'sendQuestion',
|
sendQuestion = 'sendQuestion',
|
||||||
editQuestion = 'editQuestion',
|
editQuestion = 'editQuestion'
|
||||||
|
|
||||||
requestWorkflowStore = 'requestWorkflowStore',
|
|
||||||
receiveWorkflowStore = 'receiveWorkflowStore'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventBus = {
|
export const eventBus = {
|
||||||
|
|||||||
Reference in New Issue
Block a user