Feat: Workflow loop node;feat: support openai o1;perf: query extension prompt;fix: intro was not delivered when the datase was created (#2719)

* feat: loop node (#2675)

* loop node frontend

* loop-node

* fix-code

* fix version

* fix

* fix

* fix

* perf: loop array code

* perf: get histories error tip

* feat: support openai o1

* perf: query extension prompt

* feat: 4811 doc

* remove log

* fix: loop node zindex & variable picker type (#2710)

* perf: performance

* perf: workflow performance

* remove uninvalid code

* perf:code

* fix: invoice table refresh

* perf: loop node data type

* fix: loop node store assistants

* perf: target connection

* feat: loop node support help line

* perf: add default icon

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2024-09-15 22:41:05 +08:00
committed by GitHub
parent 1ebc95a282
commit 2bdda4638d
86 changed files with 2001 additions and 718 deletions

View File

@@ -54,6 +54,60 @@
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {}
},
{
"model": "o1-mini",
"name": "o1-mini",
"avatar": "/imgs/model/openai.svg",
"maxContext": 125000,
"maxResponse": 4000,
"quoteMaxToken": 120000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": false,
"datasetProcess": false,
"usedInClassify": true,
"usedInExtractFields": true,
"usedInToolCall": true,
"usedInQueryExtension": true,
"toolChoice": false,
"functionCall": false,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {
"temperature": 1,
"max_tokens": null,
"stream": false
}
},
{
"model": "o1-preview",
"name": "o1-preview",
"avatar": "/imgs/model/openai.svg",
"maxContext": 125000,
"maxResponse": 4000,
"quoteMaxToken": 120000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": false,
"datasetProcess": false,
"usedInClassify": true,
"usedInExtractFields": true,
"usedInToolCall": true,
"usedInQueryExtension": true,
"toolChoice": false,
"functionCall": false,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {
"temperature": 1,
"max_tokens": null,
"stream": false
}
}
],
"vectorModels": [

View File

@@ -39,7 +39,6 @@ const Markdown = ({
() => ({
img: Image,
pre: RewritePre,
p: (pProps: any) => <p {...pProps} dir="auto" />,
code: Code,
a: A
}),

View File

@@ -1,7 +1,16 @@
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import Head from 'next/head';
import React from 'react';
import React, { useMemo } from 'react';
const NextHead = ({ title, icon, desc }: { title?: string; icon?: string; desc?: string }) => {
const formatIcon = useMemo(() => {
if (!icon) return LOGO_ICON;
if (icon.startsWith('http') || icon.startsWith('/')) {
return icon;
}
return LOGO_ICON;
}, [icon]);
return (
<Head>
<title>{title}</title>
@@ -11,7 +20,7 @@ const NextHead = ({ title, icon, desc }: { title?: string; icon?: string; desc?:
/>
<meta httpEquiv="Content-Security-Policy" content="img-src * data:;" />
{desc && <meta name="description" content={desc} />}
{icon && <link rel="icon" href={icon} />}
{icon && <link rel="icon" href={formatIcon} />}
</Head>
);
};

View File

@@ -504,7 +504,6 @@ const ChatInput = ({
const files = Array.from(items)
.map((item) => (item.kind === 'file' ? item.getAsFile() : undefined))
.filter((file) => {
console.log(file);
return file && fileTypeFilter(file);
}) as File[];
onSelectFile(files);

View File

@@ -35,8 +35,7 @@ const RenderText = React.memo(function RenderText({
showAnimation: boolean;
text?: string;
}) {
let source = (text || '').trim();
let source = text || '';
// First empty line
// if (!source && !isLastChild) return null;

View File

@@ -8,7 +8,6 @@ import Markdown from '@/components/Markdown';
import { QuoteList } from '../ChatContainer/ChatBox/components/QuoteModal';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
@@ -337,6 +336,22 @@ export const WholeResponseContent = ({
label={t('common:core.chat.response.update_var_result')}
value={activeModule?.updateVarResult}
/>
{/* loop */}
<Row label={t('common:core.chat.response.loop_input')} value={activeModule?.loopInput} />
<Row label={t('common:core.chat.response.loop_output')} value={activeModule?.loopResult} />
{/* loopStart */}
<Row
label={t('common:core.chat.response.loop_input_element')}
value={activeModule?.loopInputValue}
/>
{/* loopEnd */}
<Row
label={t('common:core.chat.response.loop_output_element')}
value={activeModule?.loopOutputValue}
/>
</Box>
) : null;
};
@@ -525,6 +540,9 @@ export const ResponseBox = React.memo(function ResponseBox({
if (Array.isArray(item.pluginDetail)) {
helper(item.pluginDetail);
}
if (Array.isArray(item.loopDetail)) {
helper(item.loopDetail);
}
}
});
}
@@ -552,9 +570,10 @@ export const ResponseBox = React.memo(function ResponseBox({
function pretreatmentResponse(res: ChatHistoryItemResType[]): sideTabItemType[] {
return res.map((item) => {
let children: sideTabItemType[] = [];
if (!!(item?.toolDetail || item?.pluginDetail)) {
if (!!(item?.toolDetail || item?.pluginDetail || item?.loopDetail)) {
if (item?.toolDetail) children.push(...pretreatmentResponse(item?.toolDetail));
if (item?.pluginDetail) children.push(...pretreatmentResponse(item?.pluginDetail));
if (item?.loopDetail) children.push(...pretreatmentResponse(item?.loopDetail));
}
return {
@@ -611,7 +630,7 @@ export const ResponseBox = React.memo(function ResponseBox({
<>
{isPc && !useMobile ? (
<Flex overflow={'hidden'} height={'100%'}>
<Box flex={'2 0 0'} borderRight={'sm'} p={3}>
<Box flex={'2 0 0'} w={0} borderRight={'sm'} p={3}>
<Box overflow={'auto'} height={'100%'}>
<WholeResponseSideTab
response={sliderResponseList}
@@ -620,7 +639,7 @@ export const ResponseBox = React.memo(function ResponseBox({
/>
</Box>
</Box>
<Box flex={'5 0 0'} height={'100%'}>
<Box flex={'5 0 0'} w={0} height={'100%'}>
<WholeResponseContent
activeModule={activeModule}
hideTabs={hideTabs}

View File

@@ -29,18 +29,12 @@ const InvoiceTable = () => {
data: invoices,
isLoading,
Pagination,
getData,
total
} = usePagination({
api: getInvoiceRecords,
pageSize: 20,
defaultRequest: false
pageSize: 20
});
useEffect(() => {
getData(1);
}, [getData]);
return (
<MyBox isLoading={isLoading} position={'relative'} h={'100%'} overflow={'overlay'}>
<TableContainer minH={'50vh'}>

View File

@@ -18,69 +18,67 @@ async function handler(
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
res: ApiResponseType<any>
): Promise<PaginationResponse<getHistoriesResponse>> {
try {
await connectToDatabase();
const { appId, shareId, outLinkUid, teamId, teamToken, current, pageSize } =
req.body as getHistoriesBody;
const { appId, shareId, outLinkUid, teamId, teamToken, current, pageSize } =
req.body as getHistoriesBody;
const limit = shareId && outLinkUid ? 20 : 30;
const match = await (async () => {
if (shareId && outLinkUid) {
const { uid } = await authOutLink({ shareId, outLinkUid });
const match = await (async () => {
if (shareId && outLinkUid) {
const { uid } = await authOutLink({ shareId, outLinkUid });
return {
shareId,
outLinkUid: uid,
source: ChatSourceEnum.share,
updateTime: {
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
}
};
}
if (appId && teamId && teamToken) {
const { uid } = await authTeamSpaceToken({ teamId, teamToken });
return {
teamId,
appId,
outLinkUid: uid,
source: ChatSourceEnum.team
};
}
if (appId) {
const { tmbId } = await authCert({ req, authToken: true });
return {
tmbId,
appId,
source: ChatSourceEnum.online
};
}
return Promise.reject('Params are error');
})();
const [data, total] = await Promise.all([
await MongoChat.find(match, 'chatId title top customTitle appId updateTime')
.sort({ top: -1, updateTime: -1 })
.skip((current - 1) * pageSize)
.limit(pageSize),
MongoChat.countDocuments(match)
]);
return {
shareId,
outLinkUid: uid,
source: ChatSourceEnum.share,
updateTime: {
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
}
};
}
if (appId && teamId && teamToken) {
const { uid } = await authTeamSpaceToken({ teamId, teamToken });
return {
teamId,
appId,
outLinkUid: uid,
source: ChatSourceEnum.team
};
}
if (appId) {
const { tmbId } = await authCert({ req, authToken: true });
return {
tmbId,
appId,
source: ChatSourceEnum.online
};
}
})();
if (!match) {
return {
list: data.map((item) => ({
chatId: item.chatId,
updateTime: item.updateTime,
appId: item.appId,
customTitle: item.customTitle,
title: item.title,
top: item.top
})),
total
list: [],
total: 0
};
} catch (err) {
return Promise.reject(err);
}
const [data, total] = await Promise.all([
await MongoChat.find(match, 'chatId title top customTitle appId updateTime')
.sort({ top: -1, updateTime: -1 })
.skip((current - 1) * pageSize)
.limit(pageSize),
MongoChat.countDocuments(match)
]);
return {
list: data.map((item) => ({
chatId: item.chatId,
updateTime: item.updateTime,
appId: item.appId,
customTitle: item.customTitle,
title: item.title,
top: item.top
})),
total
};
}
export default NextAPI(handler);

View File

@@ -20,6 +20,7 @@ async function handler(
const {
parentId,
name,
intro,
type = DatasetTypeEnum.dataset,
avatar,
vectorModel = global.vectorModels[0].model,
@@ -47,6 +48,7 @@ async function handler(
const { _id } = await MongoDataset.create({
...parseParentIdInMongo(parentId),
name,
intro,
teamId,
tmbId,
vectorModel,

View File

@@ -3,11 +3,7 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { sseErrRes, jsonRes } from '@fastgpt/service/common/response';
import { addLog } from '@fastgpt/service/common/system/log';
import {
ChatItemValueTypeEnum,
ChatRoleEnum,
ChatSourceEnum
} from '@fastgpt/global/core/chat/constants';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d';

View File

@@ -30,6 +30,7 @@ import { compareSnapshot } from '@/web/core/workflow/utils';
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect } from 'ahooks';
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
@@ -66,26 +67,32 @@ const Header = () => {
setPast
} = useContextSelector(WorkflowContext, (v) => v);
const isPublished = useMemo(() => {
/*
Find the last saved snapshot in the past and future snapshots
*/
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved);
const [isPublished, setIsPublished] = useState(false);
useDebounceEffect(
() => {
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
return compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: nodes,
edges: edges,
chatConfig: appDetail.chatConfig
}
);
}, [future, past, nodes, edges, appDetail.chatConfig]);
const val = compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: nodes,
edges: edges,
chatConfig: appDetail.chatConfig
}
);
setIsPublished(val);
},
[future, past, nodes, edges, appDetail.chatConfig],
{
wait: 500
}
);
const { runAsync: onClickSave, loading } = useRequest2(
async ({
@@ -205,7 +212,7 @@ const Header = () => {
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
onClick={() => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);

View File

@@ -124,7 +124,7 @@ const EditForm = ({
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
const tokenLimit = useMemo(() => {
return selectedModel?.quoteMaxToken || 3000;
}, [selectedModel.quoteMaxToken]);
}, [selectedModel?.quoteMaxToken]);
return (
<>
@@ -343,7 +343,7 @@ const EditForm = ({
{/* File select */}
<Box {...BoxStyles}>
<FileSelectConfig
forbidVision={!selectedModel.vision}
forbidVision={!selectedModel?.vision}
value={appForm.chatConfig.fileSelectConfig}
onChange={(e) => {
setAppForm((state) => ({

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Flex,
@@ -30,6 +30,7 @@ import { compareSnapshot } from '@/web/core/workflow/utils';
import SaveAndPublishModal from '../WorkflowComponents/Flow/components/SaveAndPublish';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect } from 'ahooks';
const PublishHistories = dynamic(() => import('../WorkflowPublishHistoriesSlider'));
@@ -66,26 +67,33 @@ const Header = () => {
setPast
} = useContextSelector(WorkflowContext, (v) => v);
const isPublished = useMemo(() => {
/*
Find the last saved snapshot in the past and future snapshots
*/
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) || past.find((snapshot) => snapshot.isSaved);
// Check if the workflow is published
const [isPublished, setIsPublished] = useState(false);
useDebounceEffect(
() => {
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
return compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: nodes,
edges: edges,
chatConfig: appDetail.chatConfig
}
);
}, [future, past, nodes, edges, appDetail.chatConfig]);
const val = compareSnapshot(
{
nodes: savedSnapshot?.nodes,
edges: savedSnapshot?.edges,
chatConfig: savedSnapshot?.chatConfig
},
{
nodes: nodes,
edges: edges,
chatConfig: appDetail.chatConfig
}
);
setIsPublished(val);
},
[future, past, nodes, edges, appDetail.chatConfig],
{
wait: 500
}
);
const { runAsync: onClickSave, loading } = useRequest2(
async ({
@@ -205,7 +213,7 @@ const Header = () => {
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
onClick={() => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);

View File

@@ -6,7 +6,6 @@ import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import { WorkflowContext } from './context';
import { filterSensitiveNodesData } from '@/web/core/workflow/utils';
import dynamic from 'next/dynamic';
@@ -28,7 +27,6 @@ const AppCard = ({
isPublished: boolean;
}) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { feConfigs } = useSystemStore();
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
@@ -48,7 +46,7 @@ const AppCard = ({
children: [
{
icon: 'edit',
label: appT('edit_info'),
label: t('app:edit_info'),
onClick: onOpenInfoEdit
},
{
@@ -63,7 +61,7 @@ const AppCard = ({
{
children: [
{
label: appT('import_configs'),
label: t('app:import_configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
@@ -117,7 +115,6 @@ const AppCard = ({
appDetail.name,
appDetail.permission.hasWritePer,
appDetail.permission.isOwner,
appT,
currentTab,
feConfigs?.show_team_chat,
historiesDefaultData,

View File

@@ -69,7 +69,6 @@ const ImportSettings = ({ onClose }: Props) => {
async (e: File[]) => {
const file = e[0];
readJSONFile(file);
console.log(file);
},
[readJSONFile]
);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
Box,
Flex,
@@ -14,7 +14,7 @@ import type {
NodeTemplateListItemType,
NodeTemplateListType
} from '@fastgpt/global/core/workflow/type/node.d';
import { useViewport, XYPosition } from 'reactflow';
import { useReactFlow, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
@@ -36,9 +36,7 @@ import { useRouter } from 'next/router';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useI18n } from '@/web/context/I18n';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import FolderPath from '@/components/common/folder/Path';
@@ -49,6 +47,8 @@ import { cloneDeep } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
type ModuleTemplateListProps = {
isOpen: boolean;
@@ -397,7 +397,7 @@ const RenderList = React.memo(function RenderList({
const { isPc } = useSystem();
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
const { x, y, zoom } = useViewport();
const { screenToFlowPosition } = useReactFlow();
const { toast } = useToast();
const reactFlowWrapper = useContextSelector(WorkflowContext, (v) => v.reactFlowWrapper);
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
@@ -454,11 +454,11 @@ const RenderList = React.memo(function RenderList({
}
})();
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
const nodePosition = screenToFlowPosition(position);
const mouseX = nodePosition.x - 100;
const mouseY = nodePosition.y - 20;
const node = nodeTemplate2FlowNode({
const newNode = nodeTemplate2FlowNode({
template: {
...templateNode,
name: computedNewNodeName({
@@ -482,9 +482,28 @@ const RenderList = React.memo(function RenderList({
description: t(output.description as any)
}))
},
position: { x: mouseX, y: mouseY - 20 },
selected: true
position: { x: mouseX, y: mouseY },
selected: true,
t
});
const newNodes = [newNode];
if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) {
const startNode = nodeTemplate2FlowNode({
template: LoopStartNode,
position: { x: mouseX + 60, y: mouseY + 280 },
parentNodeId: newNode.id,
t
});
const endNode = nodeTemplate2FlowNode({
template: LoopEndNode,
position: { x: mouseX + 420, y: mouseY + 680 },
parentNodeId: newNode.id,
t
});
newNodes.push(startNode, endNode);
}
setNodes((state) => {
const newState = state
@@ -493,11 +512,11 @@ const RenderList = React.memo(function RenderList({
selected: false
}))
// @ts-ignore
.concat(node);
.concat(newNodes);
return newState;
});
},
[computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, x, y, zoom]
[computedNewNodeName, reactFlowWrapper, setLoading, setNodes, t, toast, screenToFlowPosition]
);
const gridStyle = useMemo(() => {

View File

@@ -7,7 +7,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const ButtonEdge = (props: EdgeProps) => {
const { nodes, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector(
const { nodes, nodeList, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector(
WorkflowContext,
(v) => v
);
@@ -27,6 +27,10 @@ const ButtonEdge = (props: EdgeProps) => {
targetHandleId,
style
} = props;
const defaultZIndex = useMemo(
() => (nodeList.find((node) => node.nodeId === source && node.parentNodeId) ? 1001 : 0),
[nodeList, source]
);
const onDelConnect = useCallback(
(id: string) => {
@@ -135,7 +139,7 @@ const ButtonEdge = (props: EdgeProps) => {
bg={'white'}
borderRadius={'17px'}
cursor={'pointer'}
zIndex={1000}
zIndex={9999}
onClick={() => onDelConnect(id)}
>
<MyIcon name={'core/workflow/closeEdge'} w={'100%'}></MyIcon>
@@ -150,7 +154,7 @@ const ButtonEdge = (props: EdgeProps) => {
w={highlightEdge ? '14px' : '10px'}
h={highlightEdge ? '14px' : '10px'}
// bg={'white'}
zIndex={highlightEdge ? 1000 : 0}
zIndex={highlightEdge ? 1000 : defaultZIndex}
>
<MyIcon
name={'core/workflow/edgeArrow'}
@@ -199,8 +203,7 @@ const ButtonEdge = (props: EdgeProps) => {
return {
...style,
strokeWidth: 3,
zIndex: 2
strokeWidth: 3
};
})();

View File

@@ -1,10 +1,11 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { BoxProps } from '@chakra-ui/react';
const Container = ({ children, ...props }: BoxProps) => {
return (
<Box
<Flex
flexDirection={'column'}
px={4}
mx={2}
mb={2}
@@ -16,7 +17,7 @@ const Container = ({ children, ...props }: BoxProps) => {
{...props}
>
{children}
</Box>
</Flex>
);
};

View File

@@ -12,7 +12,7 @@ const IOTitle = ({
return (
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3} {...props}>
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} />
<Box>{text}</Box>
<Box color={'myGray.900'}>{text}</Box>
<Box flex={1} />
{inputExplanationUrl && (

View File

@@ -8,6 +8,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import { useWorkflowUtils } from './useUtils';
import { useKeyPress as useKeyPressEffect } from 'ahooks';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
export const useKeyboard = () => {
const { t } = useTranslation();
@@ -50,7 +51,9 @@ export const useKeyboard = () => {
if (!Array.isArray(parseData)) return;
// filter workflow data
const newNodes = parseData
.filter((item) => !!item.type && item.data?.unique !== true)
.filter(
(item) => !!item.type && item.data?.unique !== true && item.type !== FlowNodeTypeEnum.loop
)
.map((item) => {
const nodeId = getNanoid();
return {
@@ -64,7 +67,8 @@ export const useKeyboard = () => {
flowNodeType: item.data?.flowNodeType || '',
pluginId: item.data?.pluginId
}),
nodeId
nodeId,
parentNodeId: undefined
},
position: {
x: item.position.x + 100,
@@ -73,6 +77,7 @@ export const useKeyboard = () => {
};
});
// Reset all node to not select and concat new node
setNodes((prev) =>
prev
.map((node) => ({

View File

@@ -8,9 +8,14 @@ import {
Edge,
Node,
NodePositionChange,
XYPosition
XYPosition,
useReactFlow,
getNodesBounds,
Rect,
NodeRemoveChange,
NodeSelectionChange
} from 'reactflow';
import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
@@ -18,11 +23,18 @@ import { useKeyboard } from './useKeyboard';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { THelperLine } from '@fastgpt/global/core/workflow/type';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useMemoizedFn } from 'ahooks';
import {
Input_Template_Node_Height,
Input_Template_Node_Width
} from '@fastgpt/global/core/workflow/template/input';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
/*
Compute helper lines for snapping nodes to each other
Refer: https://reactflow.dev/examples/interaction/helper-lines
*/
Compute helper lines for snapping nodes to each other
Refer: https://reactflow.dev/examples/interaction/helper-lines
*/
type GetHelperLinesResult = {
horizontal?: THelperLine;
vertical?: THelperLine;
@@ -259,18 +271,64 @@ export const useWorkflow = () => {
const { t } = useTranslation();
const { isDowningCtrl } = useKeyboard();
const { setConnectingEdge, nodes, onNodesChange, setEdges, onEdgesChange, setHoverEdgeId } =
useContextSelector(WorkflowContext, (v) => v);
const {
setConnectingEdge,
nodes,
onNodesChange,
setEdges,
onChangeNode,
onEdgesChange,
setHoverEdgeId
} = useContextSelector(WorkflowContext, (v) => v);
const { getIntersectingNodes } = useReactFlow();
// Loop node size and position
const resetParentNodeSizeAndPosition = useMemoizedFn((rect: Rect, parentId: string) => {
const width = rect.width + 110 > 900 ? rect.width + 110 : 900;
const height = rect.height + 380 > 900 ? rect.height + 380 : 900;
// Update parentNode size and position
onChangeNode({
nodeId: parentId,
type: 'updateInput',
key: NodeInputKeyEnum.nodeWidth,
value: {
...Input_Template_Node_Width,
value: width
}
});
onChangeNode({
nodeId: parentId,
type: 'updateInput',
key: NodeInputKeyEnum.nodeHeight,
value: {
...Input_Template_Node_Height,
value: height
}
});
// Update parentNode position
onNodesChange([
{
id: parentId,
type: 'position',
position: {
x: rect.x - 50,
y: rect.y - 280
}
}
]);
});
/* helper line */
const [helperLineHorizontal, setHelperLineHorizontal] = useState<THelperLine>();
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>();
const customApplyNodeChanges = (changes: NodeChange[], nodes: Node[]) => {
const positionChange =
changes[0].type === 'position' && changes[0].dragging ? changes[0] : undefined;
const checkNodeHelpLine = useMemoizedFn((change: NodeChange, nodes: Node[]) => {
const positionChange = change.type === 'position' && change.dragging ? change : undefined;
if (changes.length === 1 && positionChange?.position) {
if (positionChange?.position) {
// 只判断3000px 内的 nodes并按从近到远的顺序排序
const filterNodes = nodes
.filter((node) => {
@@ -303,42 +361,182 @@ export const useWorkflow = () => {
setHelperLineHorizontal(undefined);
setHelperLineVertical(undefined);
}
};
});
// Check if a node is placed on top of a loop node
const checkNodeOverLoopNode = useMemoizedFn((node: Node) => {
if (!node) return;
// 获取所有与当前节点相交的节点
const intersections = getIntersectingNodes(node);
// 获取所有与当前节点相交的节点中,类型为 loop 的节点
const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop);
const unSupportedTypes = [
FlowNodeTypeEnum.workflowStart,
FlowNodeTypeEnum.loop,
FlowNodeTypeEnum.pluginInput,
FlowNodeTypeEnum.pluginOutput,
FlowNodeTypeEnum.systemConfig
];
if (parentNode && !node.data.parentNodeId) {
if (unSupportedTypes.includes(node.type as FlowNodeTypeEnum)) {
return toast({
status: 'warning',
title: t('workflow:can_not_loop')
});
}
onChangeNode({
nodeId: node.id,
type: 'attr',
key: 'parentNodeId',
value: parentNode.id
});
// 删除当前节点与其他节点的连接
setEdges((state) =>
state.filter((edge) => edge.source !== node.id && edge.target !== node.id)
);
const childNodes = [...nodes.filter((n) => n.data.parentNodeId === parentNode.id), node];
const rect = getNodesBounds(childNodes);
resetParentNodeSizeAndPosition(rect, parentNode.id);
}
});
/* node */
const handleNodesChange = (changes: NodeChange[]) => {
const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, node: Node) => {
if (node.data.forbidDelete) {
return toast({
status: 'warning',
title: t('common:core.workflow.Can not delete node')
});
}
// If the node has child nodes, remove the child nodes
if (nodes.some((n) => n.data.parentNodeId === node.id)) {
const childNodes = nodes.filter((n) => n.data.parentNodeId === node.id);
const childNodeIds = childNodes.map((n) => n.id);
const childNodesChange = childNodes.map((node) => ({
...change,
id: node.id
}));
onNodesChange(childNodesChange);
setEdges((state) =>
state.filter(
(edge) =>
edge.source !== change.id &&
edge.target !== change.id &&
!childNodeIds.includes(edge.source) &&
!childNodeIds.includes(edge.target)
)
);
return;
}
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
});
const handleSelectNode = useMemoizedFn((change: NodeSelectionChange) => {
// If the node is not selected and the Ctrl key is pressed, select the node
if (change.selected === false && isDowningCtrl) {
change.selected = true;
}
});
const handlePositionNode = useMemoizedFn(
(change: NodePositionChange, node: Node<FlowNodeItemType>) => {
const parentNode: Record<string, 1> = {
[FlowNodeTypeEnum.loop]: 1
};
// If node is a child node, move child node and reset parent node
if (node.data.parentNodeId) {
const parentId = node.data.parentNodeId;
const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId);
checkNodeHelpLine(change, childNodes);
resetParentNodeSizeAndPosition(getNodesBounds(childNodes), parentId);
}
// If node is parent node, move parent node and child nodes
else if (parentNode[node.data.flowNodeType]) {
// It will update the change value.
checkNodeHelpLine(
change,
nodes.filter((node) => !node.data.parentNodeId)
);
// Compute the child nodes' position
const parentId = node.id;
const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId);
const initPosition = node.position;
const deltaX = change.position?.x ? change.position.x - initPosition.x : 0;
const deltaY = change.position?.y ? change.position.y - initPosition.y : 0;
const childNodesChange: NodePositionChange[] = childNodes.map((node) => {
if (change.dragging) {
const position = {
x: node.position.x + deltaX,
y: node.position.y + deltaY
};
return {
...change,
id: node.id,
position,
positionAbsolute: position
};
} else {
return {
...change,
id: node.id
};
}
});
onNodesChange(childNodesChange);
} else {
checkNodeHelpLine(
change,
nodes.filter((node) => !node.data.parentNodeId)
);
}
}
);
const handleNodesChange = useMemoizedFn((changes: NodeChange[]) => {
for (const change of changes) {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
if (node && node.data.forbidDelete) {
return toast({
status: 'warning',
title: t('common:core.workflow.Can not delete node')
});
} else {
return (() => {
onNodesChange(changes);
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
})();
if (node) {
handleRemoveNode(change, node);
}
} else if (change.type === 'select') {
handleSelectNode(change);
} else if (change.type === 'position') {
const node = nodes.find((n) => n.id === change.id);
if (node) {
handlePositionNode(change, node);
}
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
change.selected = true;
}
}
customApplyNodeChanges(changes, nodes);
// default changes
onNodesChange(changes);
};
});
const handleEdgeChange = useCallback(
(changes: EdgeChange[]) => {
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
onEdgesChange(changes);
},
[onEdgesChange]
);
const onNodeDragStop = useCallback(
(_: any, node: Node) => {
checkNodeOverLoopNode(node);
},
[checkNodeOverLoopNode]
);
/* connect */
const onConnectStart = useCallback(
(event: any, params: OnConnectStartParams) => {
@@ -403,7 +601,8 @@ export const useWorkflow = () => {
onEdgeMouseEnter,
onEdgeMouseLeave,
helperLineHorizontal,
helperLineVertical
helperLineVertical,
onNodeDragStop
};
};

View File

@@ -52,7 +52,10 @@ const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode')),
[FlowNodeTypeEnum.userSelect]: dynamic(() => import('./nodes/NodeUserSelect'))
[FlowNodeTypeEnum.userSelect]: dynamic(() => import('./nodes/NodeUserSelect')),
[FlowNodeTypeEnum.loop]: dynamic(() => import('./nodes/Loop/NodeLoop')),
[FlowNodeTypeEnum.loopStart]: dynamic(() => import('./nodes/Loop/NodeLoopStart')),
[FlowNodeTypeEnum.loopEnd]: dynamic(() => import('./nodes/Loop/NodeLoopEnd'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
@@ -73,7 +76,8 @@ const Workflow = () => {
onEdgeMouseEnter,
onEdgeMouseLeave,
helperLineHorizontal,
helperLineVertical
helperLineVertical,
onNodeDragStop
} = useWorkflow();
const {
@@ -146,6 +150,7 @@ const Workflow = () => {
panOnScroll: true
}
: {})}
onNodeDragStop={onNodeDragStop}
>
<FlowController />
<HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />

View File

@@ -0,0 +1,88 @@
/*
The loop node has controllable width and height properties, which serve as the parent node of loopFlow.
When the childNodes of loopFlow change, it automatically calculates the rectangular width, height, and position of the childNodes,
thereby further updating the width and height properties of the loop node.
*/
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import React, { useEffect, useMemo } from 'react';
import { Background, NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
import IOTitle from '../../components/IOTitle';
import { useTranslation } from 'react-i18next';
import RenderInput from '../render/RenderInput';
import { Box } from '@chakra-ui/react';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import RenderOutput from '../render/RenderOutput';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs } = data;
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const { nodeWidth, nodeHeight } = useMemo(() => {
return {
nodeWidth: inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value,
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value
};
}, [inputs]);
const childrenNodeIdList = useMemo(() => {
return JSON.stringify(
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
);
}, [nodeId, nodeList]);
useEffect(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.childrenNodeIdList,
value: {
...Input_Template_Children_Node_List,
value: JSON.parse(childrenNodeIdList)
}
});
}, [childrenNodeIdList]);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
maxW={'full'}
minW={900}
minH={900}
w={nodeWidth}
h={nodeHeight}
menuForbid={{
copy: true
}}
{...data}
>
<Container position={'relative'} flex={1}>
<IOTitle text={t('common:common.Input')} />
<Box mb={6} maxW={'360'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Box>
<FormLabel required fontWeight={'medium'} mb={3} color={'myGray.600'}>
{t('workflow:loop_body')}
</FormLabel>
<Box flex={1} position={'relative'} border={'base'} bg={'myGray.100'} rounded={'8px'}>
<Background />
</Box>
</Container>
<Container>
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
}, [selected, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
return Render;
};
export default React.memo(NodeLoop);

View File

@@ -0,0 +1,93 @@
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import Reference from '../render/RenderInput/templates/Reference';
import { Box } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { AppContext } from '../../../../context';
import { useTranslation } from 'react-i18next';
import { getGlobalVariableNode } from '@/web/core/workflow/adapt';
const typeMap = {
[WorkflowIOValueTypeEnum.string]: WorkflowIOValueTypeEnum.arrayString,
[WorkflowIOValueTypeEnum.number]: WorkflowIOValueTypeEnum.arrayNumber,
[WorkflowIOValueTypeEnum.boolean]: WorkflowIOValueTypeEnum.arrayBoolean,
[WorkflowIOValueTypeEnum.object]: WorkflowIOValueTypeEnum.arrayObject,
[WorkflowIOValueTypeEnum.any]: WorkflowIOValueTypeEnum.arrayAny
};
const NodeLoopEnd = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { nodeId, inputs, parentNodeId } = data;
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { t } = useTranslation();
const inputItem = useMemo(
() => inputs.find((input) => input.key === NodeInputKeyEnum.loopEndInput),
[inputs]
);
// Get loopEnd input value type
const valueType = useMemo(() => {
if (!inputItem) return;
const referenceNode = [
...nodeList,
getGlobalVariableNode({ nodes: nodeList, t, chatConfig: appDetail.chatConfig })
].find((node) => node.nodeId === inputItem.value[0]);
return referenceNode?.outputs.find((output) => output.id === inputItem?.value[1])
?.valueType as keyof typeof typeMap;
}, [appDetail.chatConfig, inputItem, nodeList, t]);
useEffect(() => {
if (!valueType) return;
const parentNode = nodeList.find((node) => node.nodeId === parentNodeId);
const parentNodeOutput = parentNode?.outputs.find(
(output) => output.key === NodeOutputKeyEnum.loopArray
);
if (parentNode && parentNodeOutput) {
onChangeNode({
nodeId: parentNode.nodeId,
type: 'updateOutput',
key: NodeOutputKeyEnum.loopArray,
value: {
...parentNodeOutput,
valueType: typeMap[valueType] ?? WorkflowIOValueTypeEnum.arrayAny
}
});
}
}, [valueType, nodeList, nodeId, onChangeNode, parentNodeId]);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
{...data}
w={'420px'}
menuForbid={{
copy: true,
delete: true,
debug: true
}}
>
<Box px={4} pb={4}>
{inputItem && <Reference item={inputItem} nodeId={nodeId} />}
</Box>
</NodeCard>
);
}, [data, inputItem, nodeId, selected]);
return Render;
};
export default React.memo(NodeLoopEnd);

View File

@@ -0,0 +1,148 @@
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { useTranslation } from 'react-i18next';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import { Box, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import MyIcon from '@fastgpt/web/components/common/Icon';
const typeMap = {
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string,
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number,
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean,
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object,
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any
};
const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId } = data;
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const loopStartNode = useMemo(
() => nodeList.find((node) => node.nodeId === nodeId),
[nodeList, nodeId]
);
// According to the variable referenced by parentInput, find the output of the corresponding node and take its output valueType
const loopItemInputType = useMemo(() => {
const parentNode = nodeList.find((node) => node.nodeId === loopStartNode?.parentNodeId);
const parentArrayInput = parentNode?.inputs.find(
(input) => input.key === NodeInputKeyEnum.loopInputArray
);
return parentArrayInput?.value
? (nodeList
.find((node) => node.nodeId === parentArrayInput?.value[0])
?.outputs.find((output) => output.id === parentArrayInput?.value[1])
?.valueType as keyof typeof typeMap)
: undefined;
}, [loopStartNode?.parentNodeId, nodeList]);
// Auth update loopStartInput output
useEffect(() => {
const loopArrayOutput = loopStartNode?.outputs.find(
(output) => output.key === NodeOutputKeyEnum.loopStartInput
);
// if loopItemInputType is undefined, delete loopStartInput output
if (!loopItemInputType && loopArrayOutput) {
onChangeNode({
nodeId,
type: 'delOutput',
key: NodeOutputKeyEnum.loopStartInput
});
}
// if loopItemInputType is not undefined, and has no loopArrayOutput, add loopStartInput output
if (loopItemInputType && !loopArrayOutput) {
onChangeNode({
nodeId,
type: 'addOutput',
value: {
id: NodeOutputKeyEnum.loopStartInput,
key: NodeOutputKeyEnum.loopStartInput,
label: t('workflow:Array_element'),
type: FlowNodeOutputTypeEnum.static,
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
}
});
}
// if loopItemInputType is not undefined, and has loopArrayOutput, update loopStartInput output
if (loopItemInputType && loopArrayOutput) {
onChangeNode({
nodeId,
type: 'updateOutput',
key: NodeOutputKeyEnum.loopStartInput,
value: {
...loopArrayOutput,
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
}
});
}
}, [loopStartNode?.outputs, nodeId, onChangeNode, loopItemInputType, t]);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
{...data}
w={'420px'}
h={'176px'}
menuForbid={{
copy: true,
delete: true,
debug: true
}}
>
<Box px={4}>
{!loopItemInputType ? (
<EmptyTip text={t('workflow:loop_start_tip')} py={0} mt={0} iconSize={'32px'} />
) : (
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('common:core.module.variable.variable name')}
</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>
<Flex alignItems={'center'}>
<MyIcon
name={'core/workflow/inputType/array'}
w={'14px'}
mr={1}
color={'primary.600'}
/>
{t('workflow:Array_element')}
</Flex>
</Td>
<Td>{typeMap[loopItemInputType]}</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
</Box>
)}
</Box>
</NodeCard>
);
}, [data, loopItemInputType, selected, t]);
return Render;
};
export default React.memo(NodeLoopStart);

View File

@@ -1,34 +1,37 @@
import React from 'react';
import React, { useMemo } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from './render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import Container from '../components/Container';
import RenderInput from './render/RenderInput';
import RenderToolInput from './render/RenderToolInput';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
{/* <RenderOutput nodeId={nodeId} flowOutputList={outputs} /> */}
</Container>
</NodeCard>
);
const Render = useMemo(() => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
{/* <RenderOutput nodeId={nodeId} flowOutputList={outputs} /> */}
</Container>
</NodeCard>
);
}, [splitToolInputs, inputs, nodeId, selected, data]);
return Render;
};
export default React.memo(NodeAnswer);

View File

@@ -130,12 +130,16 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
[nodeId, onChangeNode, t]
);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
</NodeCard>
);
const Render = useMemo(() => {
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
</Container>
</NodeCard>
);
}, [CustomComponent, data, inputs, nodeId, selected]);
return Render;
};
export default React.memo(NodeCQNode);

View File

@@ -23,11 +23,11 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const { nodeId, inputs, outputs } = data;
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
const { splitToolInputs, onChangeNode, onResetNode } = useContextSelector(
WorkflowContext,
(ctx) => ctx
);
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const { ConfirmModal, openConfirm } = useConfirm({
content: workflowT('code.Reset template confirm')
});
@@ -73,31 +73,37 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
);
}
};
}, [nodeId, onChangeNode, openConfirm, workflowT]);
}, [data, nodeId, onChangeNode, onResetNode, openConfirm, workflowT]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<Container>
<IOTitle text={t('common:common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<ConfirmModal />
</NodeCard>
);
const Render = useMemo(() => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
<Container>
<IOTitle text={t('common:common.Input')} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}
CustomComponent={CustomComponent}
/>
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<ConfirmModal />
</NodeCard>
);
}, [ConfirmModal, CustomComponent, data, inputs, nodeId, outputs, selected, splitToolInputs, t]);
return Render;
};
export default React.memo(NodeCode);

View File

@@ -26,33 +26,110 @@ import ValueTypeLabel from './render/ValueTypeLabel';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import { getWebLLMModel } from '@/web/common/system/utils';
import { useMemoizedFn } from 'ahooks';
const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { llmModelList } = useSystemStore();
const { nodeId, inputs, outputs } = data;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const quoteList = useMemo(() => inputs.filter((item) => item.canEdit), [inputs]);
const Reference = useMemoizedFn(
({ nodeId, inputChildren }: { nodeId: string; inputChildren: FlowNodeInputItemType }) => {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const tokenLimit = useMemo(() => {
let maxTokens = 13000;
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
nodeList.forEach((item) => {
if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000;
const { referenceList, formatValue } = useReference({
nodeId,
valueType: inputChildren.valueType,
value: inputChildren.value
});
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
return maxTokens;
}, [nodeList, llmModelList]);
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value:
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e
}
});
},
[inputChildren, nodeId, nodeList, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: inputChildren.key
});
}, [inputChildren.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={inputChildren.required}>{t(inputChildren.label as any)}</FormLabel>
{/* value */}
<ValueTypeLabel
valueType={inputChildren.valueType}
valueDesc={inputChildren.valueDesc}
/>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={onDel}
/>
</Flex>
<ReferSelector
placeholder={t(
(inputChildren.referencePlaceholder as any) ||
t('common:core.module.Dataset quote.select')
)}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
</>
);
}
);
const CustomComponent = useMemo(() => {
const quoteList = inputs.filter((item) => item.canEdit);
const tokenLimit = (() => {
let maxTokens = 13000;
nodeList.forEach((item) => {
if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
return maxTokens;
})();
return {
[NodeInputKeyEnum.datasetMaxTokens]: (item: FlowNodeInputItemType) => (
<Box px={2}>
@@ -115,98 +192,23 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
);
}
};
}, [nodeId, onChangeNode, quoteList, t, tokenLimit]);
}, [Reference, inputs, nodeId, nodeList, onChangeNode, t, llmModelList]);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container position={'relative'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
{/* {RenderQuoteList} */}
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
const Render = useMemo(() => {
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<Container position={'relative'}>
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
{/* {RenderQuoteList} */}
</Container>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
}, [CustomComponent, data, inputs, nodeId, outputs, selected, t]);
return Render;
};
export default React.memo(NodeDatasetConcat);
function Reference({
nodeId,
inputChildren
}: {
nodeId: string;
inputChildren: FlowNodeInputItemType;
}) {
const { t } = useTranslation();
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: inputChildren.valueType,
value: inputChildren.value
});
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value:
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e
}
});
},
[inputChildren, nodeId, nodeList, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
nodeId,
type: 'delInput',
key: inputChildren.key
});
}, [inputChildren.key, nodeId, onChangeNode]);
return (
<>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={inputChildren.required}>{t(inputChildren.label as any)}</FormLabel>
{/* value */}
<ValueTypeLabel valueType={inputChildren.valueType} valueDesc={inputChildren.valueDesc} />
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
color={'myGray.500'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.600' }}
onClick={onDel}
/>
</Flex>
<ReferSelector
placeholder={t(
(inputChildren.referencePlaceholder as any) ||
t('common:core.module.Dataset quote.select')
)}
list={referenceList}
value={formatValue}
onSelect={onSelect}
/>
</>
);
}

View File

@@ -47,9 +47,10 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const requestUrl = inputs.find(
(item) => item.key === NodeInputKeyEnum.httpReqUrl
) as FlowNodeInputItemType;
const requestUrl = useMemo(
() => inputs.find((item) => item.key === NodeInputKeyEnum.httpReqUrl) as FlowNodeInputItemType,
[inputs]
);
const { userInfo, initUserInfo } = useUserStore();
@@ -217,63 +218,90 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
}
);
// not config laf
if (!token || !appid) {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<ConfigLaf />
</NodeCard>
);
} else {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
{/* select function */}
<MySelect
isLoading={isLoadingFunctions}
list={lafFunctionSelectList}
placeholder={t('common:core.module.laf.Select laf function')}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e
}
});
}}
value={selectedFunction}
/>
{/* auto set params and go to edit */}
{!!selectedFunction && (
<Flex justifyContent={'flex-end'} mt={2} gap={2}>
<Button isLoading={isSyncing} variant={'grayBase'} size={'sm'} onClick={onSyncParams}>
{t('common:core.module.Laf sync params')}
</Button>
<Button
variant={'grayBase'}
size={'sm'}
onClick={() => {
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
const Render = useMemo(() => {
// not config laf
if (!token || !appid) {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<ConfigLaf />
</NodeCard>
);
} else {
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
{/* select function */}
<MySelect
isLoading={isLoadingFunctions}
list={lafFunctionSelectList}
placeholder={t('common:core.module.laf.Select laf function')}
onchange={(e) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: e
}
});
}}
value={selectedFunction}
/>
{/* auto set params and go to edit */}
{!!selectedFunction && (
<Flex justifyContent={'flex-end'} mt={2} gap={2}>
<Button
isLoading={isSyncing}
variant={'grayBase'}
size={'sm'}
onClick={onSyncParams}
>
{t('common:core.module.Laf sync params')}
</Button>
<Button
variant={'grayBase'}
size={'sm'}
onClick={() => {
const lafFunction = lafData?.lafFunctions.find(
(item) => item.requestUrl === selectedFunction
);
if (!lafFunction) return;
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`;
window.open(url, '_blank');
}}
>
{t('common:plugin.go to laf')}
</Button>
</Flex>
)}
</Container>
{!!selectedFunction && <RenderIO {...props} />}
</NodeCard>
);
}
if (!lafFunction) return;
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}?templateid=FastGPT_Laf`;
window.open(url, '_blank');
}}
>
{t('common:plugin.go to laf')}
</Button>
</Flex>
)}
</Container>
{!!selectedFunction && <RenderIO {...props} />}
</NodeCard>
);
}
}, [
token,
appid,
selected,
data,
isLoadingFunctions,
lafFunctionSelectList,
t,
selectedFunction,
isSyncing,
onSyncParams,
props,
onChangeNode,
nodeId,
requestUrl,
lafData?.lafFunctions,
lafData?.lafApp?.appid,
feConfigs.lafEnv
]);
return Render;
};
export default React.memo(NodeLaf);

View File

@@ -414,7 +414,9 @@ const FieldEditModal = ({
{showValueTypeSelect ? (
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
list={valueTypeSelectList}
list={valueTypeSelectList.filter(
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
setValue('valueType', e);

View File

@@ -2,7 +2,6 @@ import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
const VariableTable = ({
variables = [],
@@ -14,7 +13,6 @@ const VariableTable = ({
onDelete: (key: string) => void;
}) => {
const { t } = useTranslation();
const { workflowT } = useI18n();
const showToolColumn = variables.some((item) => item.isTool);
return (
@@ -27,7 +25,7 @@ const VariableTable = ({
{t('common:core.module.variable.variable name')}
</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
{showToolColumn && <Th>{workflowT('tool_input')}</Th>}
{showToolColumn && <Th>{t('workflow:tool_input')}</Th>}
<Th borderBottomRightRadius={'none !important'}></Th>
</Tr>
</Thead>

View File

@@ -21,39 +21,43 @@ const NodeSimple = ({
const { t } = useTranslation();
const splitToolInputs = useContextSelector(WorkflowContext, (ctx) => ctx.splitToolInputs);
const { nodeId, inputs, outputs } = data;
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const filterHiddenInputs = useMemo(() => commonInputs.filter((item) => true), [commonInputs]);
const Render = useMemo(() => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
const filterHiddenInputs = commonInputs.filter((item) => true);
return (
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
{filterHiddenInputs.length > 0 && (
<>
<Container>
<IOTitle
text={t('common:common.Input')}
inputExplanationUrl={data.inputExplanationUrl}
/>
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
</Container>
</>
)}
{outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && (
<>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</>
)}
</NodeCard>
);
return (
<NodeCard minW={minW} maxW={maxW} selected={selected} {...data}>
{isTool && (
<>
<Container>
<RenderToolInput nodeId={nodeId} inputs={inputs} />
</Container>
</>
)}
{filterHiddenInputs.length > 0 && (
<>
<Container>
<IOTitle
text={t('common:common.Input')}
inputExplanationUrl={data.inputExplanationUrl}
/>
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
</Container>
</>
)}
{outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length > 0 && (
<>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</>
)}
</NodeCard>
);
}, [splitToolInputs, inputs, nodeId, minW, maxW, selected, data, t, outputs]);
return Render;
};
export default React.memo(NodeSimple);

View File

@@ -1,4 +1,4 @@
import React, { Dispatch, useMemo, useTransition } from 'react';
import React, { Dispatch, useMemo } from 'react';
import { NodeProps } from 'reactflow';
import { Box } from '@chakra-ui/react';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';

View File

@@ -119,23 +119,49 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
}: {
nodeId: string;
}) {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
const { showHandle, LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId);
const connectingNode = nodeList.find((node) => node.nodeId === connectingEdge?.nodeId);
const sourceEdges = edges.filter((edge) => edge.target === connectingNode?.nodeId);
const connectingNodeSourceNodeIds = sourceEdges.map((edge) => edge.source);
const connectingNodeSourceNodeIdMap = new Map<string, number>();
let forbidConnect = false;
edges.forEach((edge) => {
if (edge.target === connectingNode?.nodeId) {
connectingNodeSourceNodeIdMap.set(edge.source, 1);
} else if (edge.target === nodeId) {
// Node has be connected tool, it cannot be connect by other handle
if (edge.targetHandle === NodeOutputKeyEnum.selectedTools) {
forbidConnect = true;
}
// The same source handle cannot connect to the same target node
if (
connectingEdge &&
connectingEdge.handleId === edge.sourceHandle &&
edge.target === nodeId
) {
forbidConnect = true;
}
}
});
const showHandle = (() => {
if (forbidConnect) return false;
if (!node) return false;
// Tool connecting
if (connectingEdge && connectingEdge.handleId === NodeOutputKeyEnum.selectedTools)
return false;
// Unable to connect oneself
if (connectingEdge && connectingEdge.nodeId === nodeId) return false;
// Not the same parent node
if (connectingNode && connectingNode?.parentNodeId !== node?.parentNodeId) return false;
// Unable to connect to the source node
if (connectingNodeSourceNodeIds.includes(nodeId)) return false;
if (connectingNodeSourceNodeIdMap.has(nodeId)) return false;
return true;
})();
@@ -150,6 +176,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
handleId={handleId}
position={Position.Left}
translate={[-2, 0]}
showHandle={showHandle}
/>
);
})();
@@ -164,6 +191,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
handleId={handleId}
position={Position.Right}
translate={[2, 0]}
showHandle={showHandle}
/>
);
})();
@@ -178,6 +206,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
handleId={handleId}
position={Position.Top}
translate={[0, -2]}
showHandle={showHandle}
/>
);
})();
@@ -192,6 +221,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
handleId={handleId}
position={Position.Bottom}
translate={[0, 2]}
showHandle={showHandle}
/>
);
})();
@@ -205,14 +235,14 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
};
}, [connectingEdge, edges, nodeId, nodeList]);
return showHandle ? (
return (
<>
{LeftHandle}
{rightHandle}
{topHandle}
{bottomHandle}
</>
) : null;
);
});
export default function Dom() {

View File

@@ -117,6 +117,7 @@ const MySourceHandle = React.memo(function MySourceHandle({
if (!node) return null;
if (connectingEdge?.handleId === NodeOutputKeyEnum.selectedTools) return null;
return <>{RenderHandle}</>;
});
@@ -136,18 +137,16 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
position,
translate,
highlightStyle,
connectedStyle
connectedStyle,
showHandle
}: Props & {
showHandle: boolean;
highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>;
}) {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { connectingEdge, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const node = useMemo(() => nodeList.find((node) => node.nodeId === nodeId), [nodeList, nodeId]);
const connected = edges.some((edge) => edge.targetHandle === handleId);
const connectedEdges = edges.filter((edge) => edge.target === nodeId);
const translateStr = useMemo(() => {
if (!translate) return '';
@@ -190,30 +189,6 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
return;
}, [connected, connectingEdge, connectedStyle, highlightStyle, transform]);
const showHandle = useMemo(() => {
if (!node) return false;
// check tool connected
if (
edges.some(
(edge) => edge.target === nodeId && edge.targetHandle === NodeOutputKeyEnum.selectedTools
)
) {
return false;
}
if (connectingEdge?.handleId && !connectingEdge.handleId?.includes('source')) return false;
// From same source node and same handle
if (
connectedEdges.some(
(item) => item.sourceHandle === connectingEdge?.handleId && item.target === nodeId
)
)
return false;
return true;
}, [connectedEdges, connectingEdge?.handleId, edges, node, nodeId]);
const RenderHandle = useMemo(() => {
return (
<Handle
@@ -237,7 +212,11 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
return RenderHandle;
});
export const TargetHandle = (props: Props) => {
export const TargetHandle = (
props: Props & {
showHandle: boolean;
}
) => {
return (
<MyTargetHandle
{...props}

View File

@@ -30,6 +30,9 @@ type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
minW?: string | number;
maxW?: string | number;
minH?: string | number;
w?: string | number;
h?: string | number;
selected?: boolean;
menuForbid?: {
debug?: boolean;
@@ -50,6 +53,9 @@ const NodeCard = (props: Props) => {
intro,
minW = '300px',
maxW = '600px',
minH = 0,
w = 'full',
h = 'full',
nodeId,
selected,
menuForbid,
@@ -222,7 +228,7 @@ const NodeCard = (props: Props) => {
</MyTooltip>
)}
</Flex>
<MenuRender nodeId={nodeId} menuForbid={menuForbid} />
<MenuRender nodeId={nodeId} menuForbid={menuForbid} nodeList={nodeList} />
<NodeIntro nodeId={nodeId} intro={intro} />
</Box>
<ConfirmSyncModal />
@@ -234,11 +240,12 @@ const NodeCard = (props: Props) => {
avatar,
t,
name,
menuForbid,
hasNewVersion,
onOpenConfirmSync,
onClickSyncVersion,
nodeTemplate?.diagram,
menuForbid,
nodeList,
intro,
ConfirmSyncModal,
onOpenCustomTitleModal,
@@ -255,13 +262,17 @@ const NodeCard = (props: Props) => {
}, [nodeId]);
return (
<Box
<Flex
flexDirection={'column'}
minW={minW}
maxW={maxW}
minH={minH}
bg={'white'}
borderWidth={'1px'}
borderRadius={'md'}
boxShadow={'1'}
w={w}
h={h}
_hover={{
boxShadow: '4',
'& .controller-menu': {
@@ -291,7 +302,7 @@ const NodeCard = (props: Props) => {
{RenderHandle}
<EditTitleModal maxLength={20} />
</Box>
</Flex>
);
};
@@ -299,16 +310,17 @@ export default React.memo(NodeCard);
const MenuRender = React.memo(function MenuRender({
nodeId,
menuForbid
menuForbid,
nodeList
}: {
nodeId: string;
menuForbid?: Props['menuForbid'];
nodeList: FlowNodeItemType[];
}) {
const { t } = useTranslation();
const { openDebugNode, DebugInputModal } = useDebug();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
const { setNodes, setEdges, onNodesChange } = useContextSelector(WorkflowContext, (v) => v);
const { computedNewNodeName } = useWorkflowUtils();
const onCopyNode = useCallback(
@@ -347,6 +359,7 @@ const MenuRender = React.memo(function MenuRender({
version: template.version
},
selected: true,
parentNodeId: undefined,
t
})
);
@@ -356,10 +369,26 @@ const MenuRender = React.memo(function MenuRender({
);
const onDelNode = useCallback(
(nodeId: string) => {
setNodes((state) => state.filter((item) => item.data.nodeId !== nodeId));
setEdges((state) => state.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
// Remove node and its child nodes
setNodes((state) =>
state.filter((item) => item.data.nodeId !== nodeId && item.data.parentNodeId !== nodeId)
);
// Remove edges connected to the node and its child nodes
const childNodeIds = nodeList
.filter((node) => node.parentNodeId === nodeId)
.map((node) => node.nodeId);
setEdges((state) =>
state.filter(
(edge) =>
edge.source !== nodeId &&
edge.target !== nodeId &&
!childNodeIds.includes(edge.target) &&
!childNodeIds.includes(edge.source)
)
);
},
[setEdges, setNodes]
[nodeList, setEdges, setNodes]
);
const Render = useMemo(() => {

View File

@@ -1,4 +1,3 @@
import { useI18n } from '@/web/context/I18n';
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import {
Box,
@@ -39,7 +38,6 @@ const FieldModal = ({
onSubmit: (e: { data: FlowNodeInputItemType; isChangeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT, commonT } = useI18n();
const { toast } = useToast();
const isEdit = !!defaultInput.key;
@@ -56,7 +54,7 @@ const FieldModal = ({
return false;
}, [customInputConfig.selectValueTypeList, inputType]);
const valueTypeSelectLit = useMemo(() => {
const valueTypeSelectList = useMemo(() => {
if (!customInputConfig.selectValueTypeList) return [];
const dataTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({
@@ -88,7 +86,7 @@ const FieldModal = ({
if (!isEdit || isChangeKey) {
toast({
status: 'warning',
title: workflowT('field_name_already_exists')
title: t('workflow:field_name_already_exists')
});
return;
}
@@ -103,7 +101,7 @@ const FieldModal = ({
});
onClose();
},
[defaultInput.key, isEdit, keys, onClose, onSubmit, toast, workflowT]
[defaultInput.key, isEdit, keys, onClose, onSubmit, toast, t]
);
const onSubmitError = useCallback(
(e: Object) => {
@@ -124,18 +122,20 @@ const FieldModal = ({
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? workflowT('edit_input') : workflowT('add_new_input')}
title={isEdit ? t('workflow:edit_input') : t('workflow:add_new_input')}
overflow={'unset'}
>
<ModalBody w={'100%'} overflow={'auto'} display={'flex'} flexDirection={['column', 'row']}>
<Stack w={'100%'} spacing={3}>
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{commonT('core.module.Data Type')}</FormLabel>
<FormLabel flex={'0 0 70px'}>{t('common:core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
w={'full'}
list={valueTypeSelectLit}
list={valueTypeSelectList.filter(
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
setValue('valueType', e);
@@ -159,7 +159,7 @@ const FieldModal = ({
</Flex>
{customInputConfig.showDescription && (
<Flex mt={3} alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('input_description')}</FormLabel>
<FormLabel flex={'0 0 70px'}>{t('workflow:input_description')}</FormLabel>
<Textarea bg={'myGray.50'} {...register('description', {})} />
</Flex>
)}
@@ -167,10 +167,10 @@ const FieldModal = ({
</ModalBody>
<ModalFooter gap={3}>
<Button variant={'whiteBase'} onClick={onClose}>
{commonT('common.Close')}
{t('common:common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{commonT('common.Confirm')}
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@@ -48,13 +48,10 @@ const InputLabel = ({ nodeId, input }: Props) => {
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Flex
alignItems={'center'}
position={'relative'}
fontWeight={'medium'}
color={'myGray.600'}
>
<FormLabel required={required}>{t(label as any)}</FormLabel>
<Flex alignItems={'center'} position={'relative'} fontWeight={'medium'}>
<FormLabel required={required} color={'myGray.600'}>
{t(label as any)}
</FormLabel>
{description && <QuestionTip ml={1} label={t(description as any)}></QuestionTip>}
</Flex>
{/* value type */}

View File

@@ -30,9 +30,11 @@ type SelectProps = {
children: {
label: string;
value: string;
valueType?: WorkflowIOValueTypeEnum;
}[];
}[];
onSelect: (val: ReferenceValueProps) => void;
popDirection?: 'top' | 'bottom';
styles?: ButtonProps;
};
@@ -77,12 +79,19 @@ const Reference = ({ item, nodeId }: RenderInputProps) => {
value: item.value
});
const popDirection = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId);
if (!node) return 'bottom';
return node.flowNodeType === FlowNodeTypeEnum.loop ? 'top' : 'bottom';
}, [nodeId, nodeList]);
return (
<ReferSelector
placeholder={t((item.referencePlaceholder as any) || 'select_reference_variable')}
list={referenceList}
value={formatValue}
onSelect={onSelect}
popDirection={popDirection}
/>
);
};
@@ -130,13 +139,17 @@ export const useReference = ({
(output) =>
valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === valueType
output.valueType === valueType ||
// When valueType is arrayAny, return all array type outputs
(valueType === WorkflowIOValueTypeEnum.arrayAny &&
output.valueType?.includes('array'))
)
.filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam)
.map((output) => {
return {
label: t((output.label as any) || ''),
value: output.id
value: output.id,
valueType: output.valueType
};
})
};
@@ -163,7 +176,13 @@ export const useReference = ({
formatValue
};
};
export const ReferSelector = ({ placeholder, value, list = [], onSelect }: SelectProps) => {
export const ReferSelector = ({
placeholder,
value,
list = [],
onSelect,
popDirection
}: SelectProps) => {
const selectItemLabel = useMemo(() => {
if (!value) {
return;
@@ -198,9 +217,10 @@ export const ReferSelector = ({ placeholder, value, list = [], onSelect }: Selec
onSelect={(e) => {
onSelect(e as ReferenceValueProps);
}}
popDirection={popDirection}
/>
);
}, [list, onSelect, placeholder, selectItemLabel, value]);
}, [list, onSelect, placeholder, popDirection, selectItemLabel, value]);
return Render;
};

View File

@@ -1,4 +1,3 @@
import { useI18n } from '@/web/context/I18n';
import {
FlowNodeOutputTypeEnum,
FlowValueTypeMap
@@ -41,7 +40,6 @@ const FieldModal = ({
onSubmit: (e: { data: FlowNodeOutputItemType; isChangeKey: boolean }) => void;
}) => {
const { t } = useTranslation();
const { workflowT, commonT } = useI18n();
const { toast } = useToast();
const isEdit = !!defaultValue.key;
@@ -57,7 +55,7 @@ const FieldModal = ({
return true;
}, [customFieldConfig.selectValueTypeList]);
const valueTypeSelectLit = useMemo(() => {
const valueTypeSelectList = useMemo(() => {
if (!customFieldConfig.selectValueTypeList) return [];
const dataTypeSelectList = Object.values(FlowValueTypeMap)
@@ -81,7 +79,7 @@ const FieldModal = ({
if (!isEdit || isChangeKey) {
toast({
status: 'warning',
title: workflowT('field_name_already_exists')
title: t('workflow:field_name_already_exists')
});
return;
}
@@ -97,7 +95,7 @@ const FieldModal = ({
});
onClose();
},
[defaultValue.key, isEdit, keys, onClose, onSubmit, toast, workflowT]
[defaultValue.key, isEdit, keys, onClose, onSubmit, toast, t]
);
const onSubmitError = useCallback(
(e: Object) => {
@@ -118,18 +116,20 @@ const FieldModal = ({
<MyModal
isOpen={true}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? workflowT('edit_input') : workflowT('add_new_input')}
title={isEdit ? t('workflow:edit_input') : t('workflow:add_new_input')}
overflow={'unset'}
>
<ModalBody w={'100%'} overflow={'auto'} display={'flex'} flexDirection={['column', 'row']}>
<Stack w={'100%'} spacing={3}>
{showValueTypeSelect && (
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{commonT('core.module.Data Type')}</FormLabel>
<FormLabel flex={'0 0 70px'}>{t('common:core.module.Data Type')}</FormLabel>
<Box flex={1}>
<MySelect<WorkflowIOValueTypeEnum>
w={'full'}
list={valueTypeSelectLit}
list={valueTypeSelectList.filter(
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
)}
value={valueType}
onchange={(e) => {
setValue('valueType', e);
@@ -154,7 +154,7 @@ const FieldModal = ({
</Flex>
{customFieldConfig.showDescription && (
<Flex mt={3} alignItems={'center'}>
<FormLabel flex={'0 0 70px'}>{workflowT('input_description')}</FormLabel>
<FormLabel flex={'0 0 70px'}>{t('workflow:input_description')}</FormLabel>
<Textarea bg={'myGray.50'} {...register('description', {})} />
</Flex>
)}
@@ -162,10 +162,10 @@ const FieldModal = ({
</ModalBody>
<ModalFooter gap={3}>
<Button variant={'whiteBase'} onClick={onClose}>
{commonT('common.Close')}
{t('common:common.Close')}
</Button>
<Button onClick={handleSubmit(onSubmitSuccess, onSubmitError)}>
{commonT('common.Confirm')}
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@@ -118,7 +118,6 @@ type WorkflowContextType = {
setConnectingEdge: React.Dispatch<React.SetStateAction<OnConnectStartParams | undefined>>;
// common function
onFixView: () => void;
splitToolInputs: (
inputs: FlowNodeInputItemType[],
nodeId: string
@@ -201,9 +200,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
): void {
throw new Error('Function not implemented.');
},
onFixView: function (): void {
throw new Error('Function not implemented.');
},
basicNodeTemplates: [],
reactFlowWrapper: null,
nodes: [],
@@ -409,13 +405,30 @@ const WorkflowContextProvider = ({
[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(() => {
return !!nodes.find((node) => node.data.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodes]);
return !!nodeList.find((node) => node.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodeList]);
const onUpdateNodeError = useMemoizedFn((nodeId: string, isError: Boolean) => {
setNodes((nodes) => {
return nodes.map((item) => {
setNodes((state) => {
return state.map((item) => {
if (item.data?.nodeId === nodeId) {
item.selected = true;
//@ts-ignore
@@ -535,17 +548,8 @@ const WorkflowContextProvider = ({
[nodeList]
);
/* function */
const onFixView = useMemoizedFn(() => {
const btn = document.querySelector('.custom-workflow-fix_view') as HTMLButtonElement;
setTimeout(() => {
btn && btn.click();
}, 100);
});
/* If the module is connected by a tool, the tool input and the normal input are separated */
const splitToolInputs = (inputs: FlowNodeInputItemType[], nodeId: string) => {
const splitToolInputs = useMemoizedFn((inputs: FlowNodeInputItemType[], nodeId: string) => {
const isTool = !!edges.find(
(edge) => edge.targetHandle === NodeOutputKeyEnum.selectedTools && edge.target === nodeId
);
@@ -558,40 +562,7 @@ const WorkflowContextProvider = ({
return !item.toolDescription;
})
};
};
const initData = useMemoizedFn(
async (e: Parameters<WorkflowContextType['initData']>[0], isInit?: boolean) => {
/*
Refresh web page, load init
*/
if (isInit && past.length > 0) {
return resetSnapshot(past[0]);
}
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const chatConfig = e.chatConfig;
if (chatConfig) {
setAppDetail((state) => ({
...state,
chatConfig
}));
}
// If it is the initial data, save the initial snapshot
if (isInit) {
saveSnapshot({
pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [],
pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [],
customTitle: t(`app:app.version_initial`),
chatConfig: appDetail.chatConfig,
isSaved: true
});
}
}
);
});
/* ui flow to store data */
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
@@ -611,9 +582,7 @@ const WorkflowContextProvider = ({
});
const flowData2StoreData = useMemoizedFn(() => {
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return storeNodes;
return uiWorkflow2StoreWorkflow({ nodes, edges });
});
/* debug */
@@ -762,7 +731,7 @@ const WorkflowContextProvider = ({
},
[appId, onChangeNode, setNodes, workflowDebugData]
);
const onStopNodeDebug = useCallback(() => {
const onStopNodeDebug = useMemoizedFn(() => {
setWorkflowDebugData(undefined);
setNodes((state) =>
state.map((node) => ({
@@ -774,8 +743,8 @@ const WorkflowContextProvider = ({
}
}))
);
}, [setNodes]);
const onStartNodeDebug = useCallback(
});
const onStartNodeDebug = useMemoizedFn(
async ({
entryNodeId,
runtimeNodes,
@@ -795,26 +764,9 @@ const WorkflowContextProvider = ({
setWorkflowDebugData(data);
onNextNodeDebug(data);
},
[onNextNodeDebug, onStopNodeDebug]
}
);
/* Version histories */
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
/* event bus */
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes,
edges
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [edges, nodes]);
/* chat test */
const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure();
const [workflowTestData, setWorkflowTestData] = useState<{
@@ -829,24 +781,20 @@ const WorkflowContextProvider = ({
const [past, setPast] = useLocalStorageState<SnapshotsType[]>(`${appId}-past`, {
defaultValue: []
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
const [future, setFuture] = useLocalStorageState<SnapshotsType[]>(`${appId}-future`, {
defaultValue: []
}) as [SnapshotsType[], (value: SetStateAction<SnapshotsType[]>) => void];
const resetSnapshot = useCallback(
(state: SnapshotsType) => {
setNodes(state.nodes);
setEdges(state.edges);
setAppDetail((detail) => ({
...detail,
chatConfig: state.chatConfig
}));
},
[setAppDetail, setEdges, setNodes]
);
const resetSnapshot = useMemoizedFn((state: SnapshotsType) => {
setNodes(state.nodes);
setEdges(state.edges);
setAppDetail((detail) => ({
...detail,
chatConfig: state.chatConfig
}));
});
const { runAsync: saveSnapshot } = useRequest2(
const saveSnapshot = useMemoizedFn(
async ({
pastNodes,
pastEdges,
@@ -893,12 +841,10 @@ const WorkflowContextProvider = ({
setFuture([]);
return true;
},
{
refreshDeps: [nodes, edges, appDetail.chatConfig, past]
}
);
// Auto save snapshot
useDebounceEffect(
() => {
if (!nodes.length) return;
@@ -913,15 +859,14 @@ const WorkflowContextProvider = ({
{ wait: 500 }
);
const undo = useCallback(() => {
const undo = useMemoizedFn(() => {
if (past[1]) {
setFuture((future) => [past[0], ...future]);
setPast((past) => past.slice(1));
resetSnapshot(past[1]);
}
}, [past, setFuture, setPast, resetSnapshot]);
const redo = useCallback(() => {
});
const redo = useMemoizedFn(() => {
const futureState = future[0];
if (futureState) {
@@ -929,7 +874,40 @@ const WorkflowContextProvider = ({
setFuture((future) => future.slice(1));
resetSnapshot(futureState);
}
}, [future, setPast, setFuture, resetSnapshot]);
});
const initData = useMemoizedFn(
async (e: Parameters<WorkflowContextType['initData']>[0], isInit?: boolean) => {
/*
Refresh web page, load init
*/
if (isInit && past.length > 0) {
return resetSnapshot(past[0]);
}
setNodes(e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || []);
setEdges(e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || []);
const chatConfig = e.chatConfig;
if (chatConfig) {
setAppDetail((state) => ({
...state,
chatConfig
}));
}
// If it is the initial data, save the initial snapshot
if (isInit) {
saveSnapshot({
pastNodes: e.nodes?.map((item) => storeNode2FlowNode({ item, t })) || [],
pastEdges: e.edges?.map((item) => storeEdgesRenderEdge({ edge: item })) || [],
customTitle: t(`app:app.version_initial`),
chatConfig: appDetail.chatConfig,
isSaved: true
});
}
}
);
// remove other app's snapshot
useEffect(() => {
@@ -943,6 +921,22 @@ const WorkflowContextProvider = ({
});
}, [appId]);
/* Version histories */
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
/* event bus */
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes,
edges
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [edges, nodes]);
const value = {
appId,
reactFlowWrapper,
@@ -986,7 +980,6 @@ const WorkflowContextProvider = ({
canRedo: !!future.length,
// function
onFixView,
splitToolInputs,
initData,
flowData2StoreDataAndCheck,

View File

@@ -25,17 +25,10 @@ export const uiWorkflow2StoreWorkflow = ({
version: item.data.version,
inputs: item.data.inputs,
outputs: item.data.outputs,
pluginId: item.data.pluginId
pluginId: item.data.pluginId,
parentNodeId: item.data.parentNodeId
}));
// get all handle
const reactFlowViewport = document.querySelector('.react-flow__viewport');
// Gets the value of data-handleid on all elements below it whose data-handleid is not empty
const handleList =
reactFlowViewport?.querySelectorAll('[data-handleid]:not([data-handleid=""])') || [];
const handleIdList = Array.from(handleList).map(
(item) => item.getAttribute('data-handleid') || ''
);
const formatEdges: StoreEdgeItemType[] = edges
.map((item) => ({
source: item.source,
@@ -43,11 +36,7 @@ export const uiWorkflow2StoreWorkflow = ({
sourceHandle: item.sourceHandle || '',
targetHandle: item.targetHandle || ''
}))
.filter((item) => item.sourceHandle && item.targetHandle)
.filter(
// Filter out edges that do not have both sourceHandle and targetHandle
(item) => handleIdList.includes(item.sourceHandle) && handleIdList.includes(item.targetHandle)
);
.filter((item) => item.sourceHandle && item.targetHandle);
return {
nodes: formatNodes,

View File

@@ -96,7 +96,7 @@ export async function generateQA(): Promise<any> {
addLog.info(`[QA Queue] Start`);
try {
const model = getLLMModel(data.model)?.model;
const modelData = getLLMModel(data.model);
const prompt = `${data.prompt || Prompt_AgentQA.description}
${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
@@ -112,10 +112,11 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
timeout: 600000
});
const chatResponse = await ai.chat.completions.create({
model,
model: modelData.model,
temperature: 0.3,
messages: await loadRequestMessages({ messages, useVision: false }),
stream: false
stream: false,
...modelData.defaultConfig
});
const answer = chatResponse.choices?.[0].message?.content || '';
@@ -150,7 +151,7 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
tmbId: data.tmbId,
tokens: await countGptMessagesTokens(messages),
billId: data.billId,
model
model: modelData.model
});
} else {
addLog.info(`QA result 0:`, { answer });

View File

@@ -16,9 +16,5 @@ export const getWebLLMModel = (model?: string) => {
export const watchWindowHidden = () => {
// @ts-ignore
if (document.hidden) {
window.windowHidden = true;
} else {
window.windowHidden = false;
}
window.windowHidden = document.hidden;
};

View File

@@ -40,16 +40,24 @@ import { workflowSystemVariables } from '../app/utils';
export const nodeTemplate2FlowNode = ({
template,
position,
selected
selected,
parentNodeId,
zIndex,
t
}: {
template: FlowNodeTemplateType;
position: XYPosition;
selected?: boolean;
parentNodeId?: string;
zIndex?: number;
t: TFunction;
}): Node<FlowNodeItemType> => {
// replace item data
const moduleItem: FlowNodeItemType = {
...template,
nodeId: getNanoid()
name: t(template.name as any),
nodeId: getNanoid(),
parentNodeId
};
return {
@@ -57,16 +65,21 @@ export const nodeTemplate2FlowNode = ({
type: moduleItem.flowNodeType,
data: moduleItem,
position: position,
selected
selected,
zIndex
};
};
export const storeNode2FlowNode = ({
item: storeNode,
selected = false,
zIndex,
parentNodeId,
t
}: {
item: StoreNodeItemType;
selected?: boolean;
zIndex?: number;
parentNodeId?: string;
t: TFunction;
}): Node<FlowNodeItemType> => {
// init some static data
@@ -84,11 +97,11 @@ export const storeNode2FlowNode = ({
// replace item data
const nodeItem: FlowNodeItemType = {
parentNodeId,
...template,
...storeNode,
avatar: template.avatar ?? storeNode.avatar,
version: storeNode.version ?? template.version ?? defaultNodeVersion,
/*
Inputs and outputs, New fields are added, not reduced
*/
@@ -150,7 +163,8 @@ export const storeNode2FlowNode = ({
type: storeNode.flowNodeType,
data: nodeItem,
selected,
position: storeNode.position || { x: 0, y: 0 }
position: storeNode.position || { x: 0, y: 0 },
zIndex
};
};
export const storeEdgesRenderEdge = ({ edge }: { edge: StoreEdgeItemType }) => {