mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
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:
@@ -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": [
|
||||
|
@@ -39,7 +39,6 @@ const Markdown = ({
|
||||
() => ({
|
||||
img: Image,
|
||||
pre: RewritePre,
|
||||
p: (pProps: any) => <p {...pProps} dir="auto" />,
|
||||
code: Code,
|
||||
a: A
|
||||
}),
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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}
|
||||
|
@@ -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'}>
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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';
|
||||
|
@@ -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);
|
||||
|
@@ -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) => ({
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -69,7 +69,6 @@ const ImportSettings = ({ onClose }: Props) => {
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
readJSONFile(file);
|
||||
console.log(file);
|
||||
},
|
||||
[readJSONFile]
|
||||
);
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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
|
||||
};
|
||||
})();
|
||||
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -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 && (
|
||||
|
@@ -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) => ({
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -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} />
|
||||
|
@@ -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);
|
@@ -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);
|
@@ -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);
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
@@ -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';
|
||||
|
@@ -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() {
|
||||
|
@@ -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}
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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>
|
||||
|
@@ -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 */}
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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>
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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 });
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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 }) => {
|
||||
|
Reference in New Issue
Block a user