4.8.13 feature (#3118)

* chore(ui): login page & workflow page (#3046)

* login page & number input & multirow select & llm select

* workflow

* adjust nodes

* New file upload (#3058)

* feat: toolNode aiNode readFileNode adapt new version

* update docker-compose

* update tip

* feat: adapt new file version

* perf: file input

* fix: ts

* feat: add chat history time label (#3024)

* feat:add chat and logs time

* feat: add chat history time label

* code perf

* code perf

---------

Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com>

* add chatType (#3060)

* pref: slow query of full text search (#3044)

* Adapt findLast api;perf: markdown zh format. (#3066)

* perf: context code

* fix: adapt findLast api

* perf: commercial plugin run error

* perf: markdown zh format

* perf: dockerfile proxy (#3067)

* fix ui (#3065)

* fix ui

* fix

* feat: support array reference multi-select (#3041)

* feat: support array reference multi-select

* fix build

* fix

* fix loop multi-select

* adjust condition

* fix get value

* array and non-array conversion

* fix plugin input

* merge func

* feat: iframe code block;perf: workflow selector type (#3076)

* feat: iframe code block

* perf: workflow selector type

* node pluginoutput check (#3074)

* feat: View will move when workflow check error;fix: ui refresh error when continuous file upload (#3077)

* fix: plugin output check

* fix: ui refresh error when continuous file upload

* feat: View will move when workflow check error

* add dispatch try catch (#3075)

* perf: workflow context split (#3083)

* perf: workflow context split

* perf: context

* 4.8.13 test (#3085)

* perf: workflow node ui

* chat iframe url

* feat: support sub route config (#3071)

* feat: support sub route config

* dockerfile

* fix upload

* delete unused code

* 4.8.13 test (#3087)

* fix: image expired

* fix: datacard navbar ui

* perf: build action

* fix: workflow file upload refresh (#3088)

* fix: http tool response (#3097)

* loop node dynamic height (#3092)

* loop node dynamic height

* fix

* fix

* feat: support push chat log (#3093)

* feat: custom uid/metadata

* to: custom info

* fix: chat push latest

* feat: add chat log envs

* refactor: move timer to pushChatLog

* fix: using precise log

---------

Co-authored-by: Finley Ge <m13203533462@163.com>

* 4.8.13 test (#3098)

* perf: loop node refresh

* rename context

* comment

* fix: ts

* perf: push chat log

* array reference check & node ui (#3100)

* feat: loop start add index (#3101)

* feat: loop start add index

* update doc

* 4.8.13 test (#3102)

* fix: loop index;edge parent check

* perf: reference invalid check

* fix: ts

* fix: plugin select files and ai response check (#3104)

* fix: plugin select files and ai response check

* perf: text editor selector;tool call tip;remove invalid image url;

* perf: select file

* perf: drop files

* feat: source id prefix env (#3103)

* 4.8.13 test (#3106)

* perf: select file

* perf: drop files

* perf: env template

* 4.8.13 test (#3107)

* perf: select file

* perf: drop files

* fix: imple mode adapt files

* perf: push chat log (#3109)

* fix: share page load title error (#3111)

* 4.8.13 perf (#3112)

* fix: share page load title error

* update file input doc

* perf: auto add file urls

* perf: auto ser loop node offset height

* 4.8.13 test (#3117)

* perf: plugin

* updat eaction

* feat: add more share config (#3120)

* feat: add more share config

* add i18n en

* fix: missing subroute (#3121)

* perf: outlink config (#3128)

* update action

* perf: outlink config

* fix: ts (#3129)

* 更新 docSite 文档内容 (#3131)

* fix: null pointer (#3130)

* fix: null pointer

* perf: not input text

* update doc url

* perf: outlink default value (#3134)

* update doc (#3136)

* 4.8.13 test (#3137)

* update doc

* perf: completions chat api

* Restore docSite content based on upstream/4.8.13-dev (#3138)

* Restore docSite content based on upstream/4.8.13-dev

* 4813.md缺少更正

* update doc (#3141)

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
Co-authored-by: Finley Ge <m13203533462@163.com>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
This commit is contained in:
Archer
2024-11-13 11:29:53 +08:00
committed by GitHub
parent e1f5483432
commit e9d52ada73
449 changed files with 7626 additions and 4180 deletions

View File

@@ -13,6 +13,7 @@ import '@/web/styles/reset.scss';
import NextHead from '@/components/common/NextHead';
import { ReactElement, useEffect } from 'react';
import { NextPage } from 'next';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
type NextPageWithLayout = NextPage & {
setLayout?: (page: ReactElement) => JSX.Element;
@@ -49,7 +50,7 @@ function App({ Component, pageProps }: AppPropsWithLayout) {
process.env.SYSTEM_DESCRIPTION ||
`${title}${t('app:intro')}`
}
icon={feConfigs?.favicon || process.env.SYSTEM_FAVICON}
icon={getWebReqUrl(feConfigs?.favicon || process.env.SYSTEM_FAVICON)}
/>
{scripts?.map((item, i) => <Script key={i} strategy="lazyOnload" {...item}></Script>)}

View File

@@ -9,7 +9,6 @@ import {
Link,
Progress,
Grid,
Image,
BoxProps
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
@@ -45,6 +44,7 @@ import StandardPlanContentList from '@/components/support/wallet/StandardPlanCon
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
const StandDetailModal = dynamic(() => import('./standardDetailModal'));
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
@@ -653,7 +653,7 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
onClick={onOpenLaf}
fontSize={'sm'}
>
<Image src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
<MyImage src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
<Box ml={2} flex={1}>
{'laf' + t('common:navbar.Account')}
</Box>

View File

@@ -43,7 +43,7 @@ const InformTable = () => {
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>{item.title}</Box>
<Box ml={2} color={'myGray.500'} flex={'1 0 0'}>
({formatTimeToChatTime(item.time)})
({t(formatTimeToChatTime(item.time) as any).replace('#', ':')})
</Box>
{!item.read && (
<Button

View File

@@ -13,6 +13,7 @@ import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import Script from 'next/script';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
const Promotion = dynamic(() => import('./components/Promotion'));
const UsageTable = dynamic(() => import('./components/UsageTable'));
@@ -128,7 +129,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
return (
<>
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
<Script src={getWebReqUrl('/js/qrcode.min.js')} strategy="lazyOnload"></Script>
<PageContainer>
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
{isPc ? (

View File

@@ -17,16 +17,8 @@ async function handler(
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
res: ApiResponseType<any>
): Promise<PaginationResponse<getHistoriesResponse>> {
const {
appId,
shareId,
outLinkUid,
teamId,
teamToken,
offset,
pageSize,
source = ChatSourceEnum.online
} = req.body as getHistoriesBody;
const { appId, shareId, outLinkUid, teamId, teamToken, offset, pageSize, source } =
req.body as getHistoriesBody;
const match = await (async () => {
if (shareId && outLinkUid) {
@@ -35,7 +27,6 @@ async function handler(
return {
shareId,
outLinkUid: uid,
source: ChatSourceEnum.share,
updateTime: {
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
}
@@ -55,7 +46,7 @@ async function handler(
return {
tmbId,
appId,
source: source
source
};
}
})();

View File

@@ -9,7 +9,7 @@ import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
@@ -67,13 +67,11 @@ async function handler(
})();
const fieldMap = {
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback time ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`,
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${
shareChat?.responseDetail || isPlugin ? `${DispatchNodeResponseKeyEnum.nodeResponse}` : ''
} `,
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`,
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`
};
const { total, histories } = await getChatItems({
@@ -85,10 +83,14 @@ async function handler(
});
// Remove important information
if (type === 'outLink' && app.type !== AppTypeEnum.plugin) {
if (shareChat && app.type !== AppTypeEnum.plugin) {
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
if (shareChat.showNodeStatus === false) {
item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool);
}
}
});
}

View File

@@ -2,7 +2,6 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import type { InitChatResponse, InitOutLinkChatProps } from '@/global/core/chat/api.d';
import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { MongoApp } from '@fastgpt/service/core/app/schema';
@@ -54,7 +53,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
storeWelcomeText: chat?.welcomeText,
isPublicFetch: false
}),
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro,

View File

@@ -5,12 +5,13 @@ import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constant
import { createFileToken } from '@fastgpt/service/support/permission/controller';
import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
export type readCollectionSourceQuery = {
export type readCollectionSourceQuery = {};
export type readCollectionSourceBody = {
collectionId: string;
};
export type readCollectionSourceBody = {};
} & ShareChatAuthProps;
export type readCollectionSourceResponse = {
type: 'url';
@@ -24,7 +25,7 @@ async function handler(
req,
authToken: true,
authApiKey: true,
collectionId: req.query.collectionId,
collectionId: req.body.collectionId,
per: ReadPermissionVal
});

View File

@@ -24,7 +24,7 @@ export type OutLinkUpdateResponse = {};
async function handler(
req: ApiRequestProps<OutLinkUpdateBody, OutLinkUpdateQuery>
): Promise<OutLinkUpdateResponse> {
const { _id, name, responseDetail, limit, app } = req.body;
const { _id, name, responseDetail, limit, app, showRawSource, showNodeStatus } = req.body;
if (!_id) {
return Promise.reject(CommonErrEnum.missingParams);
@@ -35,6 +35,8 @@ async function handler(
await MongoOutLink.findByIdAndUpdate(_id, {
name,
responseDetail,
showRawSource,
showNodeStatus,
limit,
app
});

View File

@@ -63,6 +63,8 @@ import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/u
type FastGptWebChatProps = {
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
appId?: string;
customUid?: string; // non-undefined: will be the priority provider for the logger.
metadata?: Record<string, any>;
};
export type Props = ChatCompletionCreateParams &
@@ -81,10 +83,12 @@ type AuthResponseType = {
user: UserModelSchema;
app: AppSchema;
responseDetail?: boolean;
showNodeStatus?: boolean;
authType: `${AuthUserTypeEnum}`;
apikey?: string;
canWrite: boolean;
outLinkUserId?: string;
sourceName?: string;
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -99,6 +103,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
let {
chatId,
appId,
customUid,
// share chat
shareId,
outLinkUid,
@@ -110,7 +115,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
detail = false,
messages = [],
variables = {},
responseChatItemId = getNanoid()
responseChatItemId = getNanoid(),
metadata
} = req.body as Props;
const originIp = requestIp.getClientIp(req);
@@ -122,7 +128,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
throw new Error('messages is not array');
}
/*
/*
Web params: chatId + [Human]
API params: chatId + [Human]
API params: [histories, Human]
@@ -139,41 +145,52 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return JSON.stringify(variables);
})();
/*
/*
1. auth app permission
2. auth balance
3. get app
4. parse outLink token
*/
const { teamId, tmbId, user, app, responseDetail, authType, apikey, canWrite, outLinkUserId } =
await (async () => {
// share chat
if (shareId && outLinkUid) {
return authShareChat({
shareId,
outLinkUid,
chatId,
ip: originIp,
question: startHookText
});
}
// team space chat
if (spaceTeamId && appId && teamToken) {
return authTeamSpaceChat({
teamId: spaceTeamId,
teamToken,
appId,
chatId
});
}
/* parse req: api or token */
return authHeaderRequest({
req,
const {
teamId,
tmbId,
user,
app,
responseDetail,
authType,
sourceName,
apikey,
canWrite,
outLinkUserId = customUid,
showNodeStatus
} = await (async () => {
// share chat
if (shareId && outLinkUid) {
return authShareChat({
shareId,
outLinkUid,
chatId,
ip: originIp,
question: startHookText
});
}
// team space chat
if (spaceTeamId && appId && teamToken) {
return authTeamSpaceChat({
teamId: spaceTeamId,
teamToken,
appId,
chatId
});
})();
}
/* parse req: api or token */
return authHeaderRequest({
req,
appId,
chatId
});
})();
const isPlugin = app.type === AppTypeEnum.plugin;
// Check message type
@@ -241,7 +258,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
res,
detail,
streamResponse: stream,
id: chatId
id: chatId,
showNodeStatus
});
/* start flow controller */
@@ -330,10 +348,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
newTitle,
shareId,
outLinkUid: outLinkUserId,
source,
source: sourceName || source,
content: [userQuestion, aiResponse],
metadata: {
originIp
originIp,
...metadata
}
});
}
@@ -445,7 +464,7 @@ const authShareChat = async ({
shareId: string;
chatId?: string;
}): Promise<AuthResponseType> => {
const { teamId, tmbId, user, appId, authType, responseDetail, uid } =
const { teamId, tmbId, user, appId, authType, responseDetail, showNodeStatus, uid, sourceName } =
await authOutLinkChatStart(data);
const app = await MongoApp.findById(appId).lean();
@@ -460,6 +479,7 @@ const authShareChat = async ({
}
return {
sourceName,
teamId,
tmbId,
user,
@@ -468,7 +488,8 @@ const authShareChat = async ({
apikey: '',
authType,
canWrite: false,
outLinkUserId: uid
outLinkUserId: uid,
showNodeStatus
};
};
const authTeamSpaceChat = async ({
@@ -527,6 +548,7 @@ const authHeaderRequest = async ({
teamId,
tmbId,
authType,
sourceName,
apikey
} = await authCert({
req,
@@ -593,6 +615,7 @@ const authHeaderRequest = async ({
responseDetail: true,
apikey,
authType,
sourceName,
canWrite: true
};
};

View File

@@ -180,6 +180,9 @@ const DetailLogsModal = ({
chatConfig={chat?.app?.chatConfig}
appId={appId}
chatId={chatId}
chatType="log"
showRawSource
showNodeStatus
/>
)}
</Box>

View File

@@ -129,15 +129,16 @@ const Logs = () => {
onClick={() => setDetailLogsId(item.id)}
>
<Td>
<Box>{t(ChatSourceMap[item.source]?.name || ('UnKnow' as any))}</Box>
{/* @ts-ignore */}
<Box>{t(ChatSourceMap[item.source]?.name) || item.source}</Box>
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
</Td>
<Td>
<Box>
{item.source === 'share' ? (
{!!item.outLinkUid ? (
item.outLinkUid
) : (
<Tag key={item._id} type={'fill'} colorSchema="white">
<HStack>
<Avatar
src={teamMembers.find((v) => v.tmbId === item.tmbId)?.avatar}
w="1.25rem"
@@ -145,7 +146,7 @@ const Logs = () => {
<Box fontSize={'sm'} ml={1}>
{teamMembers.find((v) => v.tmbId === item.tmbId)?.memberName}
</Box>
</Tag>
</HStack>
)}
</Box>
</Td>

View File

@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from '../Workflow/components/SaveButton';
import PublishHistories from '../PublishHistoriesSlider';
import {
WorkflowNodeEdgeContext,
WorkflowInitContext
} from '../WorkflowComponents/context/workflowInitContext';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
const Header = () => {
const { t } = useTranslation();
@@ -44,27 +49,31 @@ const Header = () => {
onClose: onCloseBackConfirm
} = useDisclosure();
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const {
flowData2StoreData,
flowData2StoreDataAndCheck,
setWorkflowTestData,
setShowHistoryModal,
showHistoryModal,
nodes,
edges,
past,
future,
setPast,
onSwitchTmpVersion,
onSwitchCloudVersion
} = useContextSelector(WorkflowContext, (v) => v);
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
const setShowHistoryModal = useContextSelector(
WorkflowEventContext,
(v) => v.setShowHistoryModal
);
const { lastAppListRouteType } = useSystemStore();
const [isPublished, setIsPublished] = useState(false);
useDebounceEffect(
() => {
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) ||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
const val = compareSnapshot(
@@ -145,7 +154,6 @@ const Header = () => {
)}
<Flex
mt={[2, 0]}
py={3}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
@@ -163,12 +171,20 @@ const Header = () => {
})}
>
{/* back */}
<MyIcon
name={'common/leftArrowLight'}
w={'1.75rem'}
cursor={'pointer'}
onClick={isPublished ? onBack : onOpenBackConfirm}
/>
<Box
_hover={{
bg: 'myGray.200'
}}
p={0.5}
borderRadius={'sm'}
>
<MyIcon
name={'common/leftArrowLight'}
w={6}
cursor={'pointer'}
onClick={isPublished ? onBack : onOpenBackConfirm}
/>
</Box>
{/* app info */}
<Box ml={1}>

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
import { ReactFlowCustomProvider, WorkflowContext } from '../WorkflowComponents/context';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
@@ -13,13 +13,15 @@ import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import Flow from '../WorkflowComponents/Flow';
import { t } from 'i18next';
import { useTranslation } from 'next-i18next';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
@@ -64,9 +66,9 @@ const WorkflowEdit = () => {
const Render = () => {
return (
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
<ReactFlowCustomProvider templates={pluginSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
</ReactFlowCustomProvider>
);
};

View File

@@ -90,7 +90,10 @@ const FeiShuEditModal = ({
<Box color="myGray.600">{t('publish:feishu_api')}</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/feishu')}
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/feishu/')
}
target={'_blank'}
ml={2}
color={'primary.500'}

View File

@@ -73,7 +73,10 @@ const FeiShu = ({ appId }: { appId: string }) => {
</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/feishu')}
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/feishu/')
}
target={'_blank'}
color={'primary.500'}
fontSize={'sm'}
@@ -147,7 +150,7 @@ const FeiShu = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@@ -2,7 +2,7 @@ import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import React, { useCallback, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Flex, FlexProps, Grid, Image, ModalBody, Switch, useTheme } from '@chakra-ui/react';
import { Box, Flex, FlexProps, Grid, ModalBody, Switch, useTheme } from '@chakra-ui/react';
import MyRadio from '@/components/common/MyRadio';
import { useForm } from 'react-hook-form';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -10,6 +10,7 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { fileToBase64 } from '@/web/common/file/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
enum UsingWayEnum {
link = 'link',
@@ -29,15 +30,15 @@ const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose
const VariableTypeList = [
{
title: <Image src={'/imgs/outlink/link.svg'} alt={''} />,
title: <MyImage src={'/imgs/outlink/link.svg'} alt={''} />,
value: UsingWayEnum.link
},
{
title: <Image src={'/imgs/outlink/iframe.svg'} alt={''} />,
title: <MyImage src={'/imgs/outlink/iframe.svg'} alt={''} />,
value: UsingWayEnum.iframe
},
{
title: <Image src={'/imgs/outlink/script.svg'} alt={''} />,
title: <MyImage src={'/imgs/outlink/script.svg'} alt={''} />,
value: UsingWayEnum.script
}
];
@@ -162,7 +163,7 @@ console.log("Chat box loaded")
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Open Icon')}</Box>
<Image
<MyImage
src={getValues('scriptOpenIcon')}
alt={''}
w={'20px'}
@@ -173,7 +174,7 @@ console.log("Chat box loaded")
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Close Icon')}</Box>
<Image
<MyImage
src={getValues('scriptCloseIcon')}
alt={''}
w={'20px'}

View File

@@ -32,7 +32,6 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useForm } from 'react-hook-form';
import { defaultOutLinkForm } from '@/web/core/app/constants';
import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type.d';
import { useRequest } from '@/web/common/hooks/useRequest';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -48,6 +47,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal'));
@@ -150,7 +150,9 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
</>
)}
<Td>
{item.lastTime ? formatTimeToChatTime(item.lastTime) : t('common:common.Un used')}
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
@@ -181,7 +183,9 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
setEditLinkData({
_id: item._id,
name: item.name,
responseDetail: item.responseDetail,
responseDetail: item.responseDetail ?? false,
showRawSource: item.showRawSource ?? false,
showNodeStatus: item.showNodeStatus ?? false,
limit: item.limit
})
},
@@ -270,27 +274,30 @@ function EditLinkModal({
const {
register,
setValue,
watch,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const responseDetail = watch('responseDetail');
const showRawSource = watch('showRawSource');
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (e: OutLinkEditType) =>
const { runAsync: onclickCreate, loading: creating } = useRequest2(
async (e: OutLinkEditType) =>
createShareChat({
...e,
appId,
type
}),
errorToast: t('common:common.Create Failed'),
onSuccess: onCreate
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: (e: OutLinkEditType) => {
return putShareChat(e);
},
{
errorToast: t('common:common.Create Failed'),
onSuccess: onCreate
}
);
const { runAsync: onclickUpdate, loading: updating } = useRequest2(putShareChat, {
errorToast: t('common:common.Update Failed'),
onSuccess: onEdit
});
@@ -300,101 +307,133 @@ function EditLinkModal({
isOpen={true}
iconSrc="/imgs/modal/shareFill.svg"
title={isEdit ? publishT('edit_link') : publishT('create_link')}
maxW={['90vw', '700px']}
w={'100%'}
h={['90vh', 'auto']}
>
<ModalBody>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
<Input
placeholder={publishT('link_name')}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
{feConfigs?.isPlus && (
<>
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'} alignItems={'center'}>
{t('common:common.Expired Time')}
</FormLabel>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>QPM</FormLabel>
<QuestionTip ml={1} label={publishT('qpm_tips' || '')}></QuestionTip>
</Flex>
<Input
max={1000}
{...register('limit.QPM', {
min: 0,
max: 1000,
valueAsNumber: true,
required: publishT('qpm_is_empty') || ''
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{t('common:support.outlink.Max usage points')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.Max usage points tip')}
></QuestionTip>
</Flex>
<Input
{...register('limit.maxUsagePoints', {
min: -1,
max: 10000000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{publishT('token_auth')}</FormLabel>
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
</Flex>
<Input
placeholder={publishT('token_auth_tips') || ''}
fontSize={'sm'}
{...register('limit.hookUrl')}
/>
</Flex>
<Link
href={getDocPath('/docs/development/openapi/share')}
target={'_blank'}
fontSize={'xs'}
color={'myGray.500'}
>
{publishT('token_auth_use_cases')}
</Link>
</>
)}
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.Response Quote')}</FormLabel>
<QuestionTip
ml={1}
label={t('support.outlink.share.Response Quote tips' || '')}
></QuestionTip>
<ModalBody
p={6}
display={['block', 'flex']}
flex={['1 0 0', 'auto']}
overflow={'auto'}
gap={4}
>
<Box pr={[0, 4]} flex={1} borderRight={['0px', '1px']} borderColor={['', 'myGray.150']}>
<Box fontSize={'sm'} fontWeight={'500'} color={'myGray.600'}>
{t('publish:basic_info')}
</Box>
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
<Input
placeholder={publishT('link_name')}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
<Switch {...register('responseDetail')} />
</Flex>
{feConfigs?.isPlus && (
<>
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'} alignItems={'center'}>
{t('common:common.Expired Time')}
</FormLabel>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>QPM</FormLabel>
<QuestionTip ml={1} label={publishT('qpm_tips' || '')}></QuestionTip>
</Flex>
<Input
max={1000}
{...register('limit.QPM', {
min: 0,
max: 1000,
valueAsNumber: true,
required: publishT('qpm_is_empty') || ''
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{t('common:support.outlink.Max usage points')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.Max usage points tip')}
></QuestionTip>
</Flex>
<Input
{...register('limit.maxUsagePoints', {
min: -1,
max: 10000000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{publishT('token_auth')}</FormLabel>
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
</Flex>
<Input
placeholder={publishT('token_auth_tips') || ''}
fontSize={'sm'}
{...register('limit.hookUrl')}
/>
</Flex>
<Link
href={getDocPath('/docs/development/openapi/share')}
target={'_blank'}
fontSize={'xs'}
color={'myGray.500'}
>
{publishT('token_auth_use_cases')}
</Link>
</>
)}
</Box>
<Box flex={1} pt={[6, 0]}>
<Box fontSize={'sm'} fontWeight={'500'} color={'myGray.600'}>
{t('publish:private_config')}
</Box>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<FormLabel>{t('publish:show_node')}</FormLabel>
<Switch {...register('showNodeStatus')} />
</Flex>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.Response Quote')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.Response Quote tips' || '')}
></QuestionTip>
</Flex>
<Switch {...register('responseDetail')} isChecked={responseDetail} />
</Flex>
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.show_complete_quote_tips' || '')}
></QuestionTip>
</Flex>
<Switch {...register('showRawSource')} isChecked={showRawSource} />
</Flex> */}
</Box>
</ModalBody>
<ModalFooter>

View File

@@ -96,7 +96,10 @@ const OffiAccountEditModal = ({
<Box color="myGray.600">{t('publish:official_account.params')}</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/official_account')}
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/official_account/')
}
target={'_blank'}
ml={2}
color={'primary.500'}

View File

@@ -75,7 +75,10 @@ const OffiAccount = ({ appId }: { appId: string }) => {
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/course/official_account')}
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/official_account/')
}
target={'_blank'}
ml={2}
color={'primary.500'}
@@ -150,7 +153,7 @@ const OffiAccount = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@@ -126,7 +126,7 @@ const Wecom = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@@ -3,6 +3,7 @@ import { Box, Image, Flex, ModalBody } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
export type ShowShareLinkModalProps = {
shareLink: string;
@@ -40,7 +41,7 @@ function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps
</Box>
</Box>
<Box mt="4" borderRadius="0.5rem" border="1px" borderStyle="solid" borderColor="myGray.200">
<Image src={img} borderRadius="0.5rem" alt="" />
<MyImage src={img} borderRadius="0.5rem" alt="" />
</Box>
</ModalBody>
</MyModal>

View File

@@ -51,6 +51,7 @@ const RouteTab = () => {
px={2}
py={0.5}
fontWeight={'medium'}
borderRadius={'sm'}
{...(currentTab === tab.id
? {
color: 'primary.700'
@@ -59,8 +60,7 @@ const RouteTab = () => {
color: 'myGray.600',
cursor: 'pointer',
_hover: {
bg: 'myGray.200',
borderRadius: 'md'
bg: 'myGray.200'
},
onClick: () => setCurrentTab(tab.id)
})}

View File

@@ -84,7 +84,7 @@ const AppCard = () => {
>
{appDetail.intro || t('common:core.app.tip.Add a intro to app')}
</Box>
<HStack alignItems={'flex-end'}>
<HStack alignItems={'center'}>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
@@ -107,7 +107,7 @@ const AppCard = () => {
<MyMenu
Button={
<IconButton
variant={'whiteBase'}
variant={'whitePrimary'}
size={['smSquare', 'mdSquare']}
icon={<MyIcon name={'more'} w={'1rem'} />}
aria-label={''}

View File

@@ -43,9 +43,6 @@ const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGSwitch = dynamic(() => import('@/components/core/app/QGSwitch'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
const ScheduledTriggerConfig = dynamic(
() => import('@/components/core/app/ScheduledTriggerConfig')
);
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
const FileSelectConfig = dynamic(() => import('@/components/core/app/FileSelect'));
@@ -455,22 +452,6 @@ const EditForm = ({
}}
/>
</Box>
{/* timer trigger */}
<Box {...BoxStyles} borderBottom={'none'}>
<ScheduledTriggerConfig
value={appForm.chatConfig.scheduledTriggerConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
scheduledTriggerConfig: e
}
}));
}}
/>
</Box>
</Box>
{isOpenDatasetSelect && (

View File

@@ -387,6 +387,7 @@ const RenderList = React.memo(function RenderList({
isInvalid={errors && Object.keys(errors).includes(input.key)}
onChange={onChange}
input={input}
setUploading={() => {}}
/>
);
}}

View File

@@ -29,6 +29,11 @@ import { useDebounceEffect } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from './components/SaveButton';
import PublishHistories from '../PublishHistoriesSlider';
import {
WorkflowNodeEdgeContext,
WorkflowInitContext
} from '../WorkflowComponents/context/workflowInitContext';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
const Header = () => {
const { t } = useTranslation();
@@ -48,20 +53,23 @@ const Header = () => {
onClose: onCloseBackConfirm
} = useDisclosure();
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const {
flowData2StoreData,
flowData2StoreDataAndCheck,
setWorkflowTestData,
setShowHistoryModal,
showHistoryModal,
nodes,
edges,
past,
future,
setPast,
onSwitchTmpVersion,
onSwitchCloudVersion
} = useContextSelector(WorkflowContext, (v) => v);
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
const setShowHistoryModal = useContextSelector(
WorkflowEventContext,
(v) => v.setShowHistoryModal
);
const { lastAppListRouteType } = useSystemStore();
@@ -70,7 +78,7 @@ const Header = () => {
useDebounceEffect(
() => {
const savedSnapshot =
future.findLast((snapshot) => snapshot.isSaved) ||
[...future].reverse().find((snapshot) => snapshot.isSaved) ||
past.find((snapshot) => snapshot.isSaved);
const val = compareSnapshot(
@@ -151,7 +159,6 @@ const Header = () => {
)}
<Flex
mt={[2, 0]}
py={3}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
@@ -169,12 +176,20 @@ const Header = () => {
})}
>
{/* back */}
<MyIcon
name={'common/leftArrowLight'}
w={'1.75rem'}
cursor={'pointer'}
onClick={isPublished ? onBack : onOpenBackConfirm}
/>
<Box
_hover={{
bg: 'myGray.200'
}}
p={0.5}
borderRadius={'sm'}
>
<MyIcon
name={'common/leftArrowLight'}
w={6}
cursor={'pointer'}
onClick={isPublished ? onBack : onOpenBackConfirm}
/>
</Box>
{/* app info */}
<Box ml={1}>

View File

@@ -11,15 +11,18 @@ import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import { useTranslation } from 'next-i18next';
import Flow from '../WorkflowComponents/Flow';
import { t } from 'i18next';
import { ReactFlowCustomProvider } from '../WorkflowComponents/context/index';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
@@ -64,9 +67,9 @@ const WorkflowEdit = () => {
const Render = () => {
return (
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
<ReactFlowCustomProvider templates={appSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
</ReactFlowCustomProvider>
);
};

View File

@@ -4,18 +4,18 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
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 { WorkflowContext } from './context';
import { filterSensitiveNodesData } from '@/web/core/workflow/utils';
import dynamic from 'next/dynamic';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import { fileDownload } from '@/web/common/file/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
@@ -31,83 +31,115 @@ const AppCard = ({
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
useContextSelector(AppContext, (v) => v);
const { showHistoryModal } = useContextSelector(WorkflowContext, (v) => v);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const InfoMenu = useCallback(
({ children }: { children: React.ReactNode }) => {
return (
<MyMenu
width={150}
Button={children}
menuList={[
{
children: [
{
icon: 'edit',
label: t('app:edit_info'),
onClick: onOpenInfoEdit
},
{
icon: 'support/team/key',
label: t('common:common.Role'),
onClick: onOpenInfoEdit
}
]
},
...(!showHistoryModal && currentTab === TabEnum.appEdit
? [
{
children: [
{
label: t('app:import_configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: ExportPopover({
chatConfig: appDetail.chatConfig,
appName: appDetail.name
}),
menuItemStyles: {
p: 0,
cursor: 'default'
}
}
]
}
]
: []),
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
? [
{
children: [
{
icon: 'support/team/memberLight',
label: t('common:common.Team Tags Set'),
onClick: onOpenTeamTagModal
}
]
}
]
: []),
...(appDetail.permission.isOwner
? [
{
children: [
{
type: 'danger' as 'danger',
icon: 'delete',
label: t('common:common.Delete'),
onClick: onDelApp
}
]
}
]
: [])
]}
/>
<MyPopover
placement={'bottom-end'}
hasArrow={false}
offset={[2, 4]}
w={'116px'}
trigger={'hover'}
Trigger={children}
>
{({ onClose }) => (
<Box p={1.5}>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenInfoEdit}
>
<MyIcon name={'edit'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:edit_info')}</Box>
</MyBox>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenInfoEdit}
>
<MyIcon name={'support/team/key'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:Role_setting')}</Box>
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenImport}
>
<MyIcon name={'common/importLight'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:import_configs')}</Box>
</MyBox>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
>
{ExportPopover({
chatConfig: appDetail.chatConfig,
appName: appDetail.name
})}
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenTeamTagModal}
>
<MyIcon name={'core/dataset/tag'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:Team_Tags')}</Box>
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
</>
)}
{appDetail.permission.isOwner && (
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
color={'red.600'}
_hover={{ bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onDelApp}
>
<MyIcon name={'delete'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('common:common.Delete')}</Box>
</MyBox>
)}
</Box>
)}
</MyPopover>
);
},
[
@@ -117,7 +149,6 @@ const AppCard = ({
appDetail.permission.isOwner,
currentTab,
feConfigs?.show_team_chat,
showHistoryModal,
onDelApp,
onOpenImport,
onOpenInfoEdit,
@@ -129,21 +160,26 @@ const AppCard = ({
const Render = useMemo(() => {
return (
<HStack>
<InfoMenu>
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} />
</InfoMenu>
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} />
<Box>
<InfoMenu>
<HStack spacing={1} cursor={'pointer'}>
<HStack
spacing={1}
cursor={'pointer'}
pl={1}
ml={-1}
borderRadius={'xs'}
_hover={{ bg: 'myGray.150' }}
>
<Box color={'myGray.900'}>{appDetail.name}</Box>
<MyIcon name={'common/select'} w={'1rem'} />
<MyIcon name={'common/select'} w={'1rem'} color={'myGray.500'} />
</HStack>
</InfoMenu>
{showSaveStatus && (
<Flex alignItems={'center'} h={'20px'} fontSize={'mini'} lineHeight={1}>
<Flex alignItems={'center'} fontSize={'mini'} lineHeight={1}>
<MyTag
py={0}
px={0}
px={1}
showDot
bg={'transparent'}
colorSchema={
@@ -211,15 +247,19 @@ function ExportPopover({
return (
<MyPopover
placement={'right-start'}
offset={[0, 0]}
offset={[0, 20]}
hasArrow
trigger={'hover'}
w={'8.6rem'}
Trigger={
<Flex align={'center'} w={'100%'} py={2} px={3}>
<Avatar src={'export'} borderRadius={'sm'} w={'1rem'} mr={3} />
{t('app:export_configs')}
</Flex>
// <Flex align={'center'} w={'100%'} py={2} px={3}>
// <Avatar src={'export'} borderRadius={'sm'} w={'1rem'} mr={3} />
// {t('app:export_configs')}
// </Flex>
<MyBox display={'flex'} size={'md'} rounded={'4px'} cursor={'pointer'}>
<MyIcon name={'export'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:export_configs')}</Box>
</MyBox>
}
>
{({ onClose }) => (

View File

@@ -50,6 +50,7 @@ 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';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { WorkflowNodeEdgeContext } from '../context/workflowInitContext';
type ModuleTemplateListProps = {
isOpen: boolean;
@@ -79,10 +80,10 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore();
const { basicNodeTemplates, hasToolNode, nodeList, appId } = useContextSelector(
WorkflowContext,
(v) => v
);
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
const { data: members = [] } = useRequest2(loadAndGetTeamMembers, {
manual: !feConfigs.isPlus
@@ -217,105 +218,120 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
}
);
const Render = useMemo(() => {
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
maxW={'100%'}
onClick={onClose}
fontSize={'sm'}
/>
<MyBox
isLoading={isLoading}
display={'flex'}
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'10px'}
left={0}
pt={'20px'}
pb={4}
h={isOpen ? 'calc(100% - 20px)' : '0'}
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'0 20px 20px 0'}
transition={'.2s ease'}
userSelect={'none'}
overflow={isOpen ? 'none' : 'hidden'}
>
{/* Header */}
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
{/* Tabs */}
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<Box flex={'1 0 0'}>
<FillRowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('common:core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('common:core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('common:core.module.template.Team app'),
value: TemplateTypeEnum.teamPlugin
}
]}
width={'100%'}
py={'5px'}
value={templateType}
onChange={(e) => {
loadNodeTemplates({
type: e as TemplateTypeEnum,
parentId: ''
});
}}
/>
</Box>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
borderColor={'myGray.300'}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
maxW={'100%'}
onClick={onClose}
fontSize={'sm'}
/>
<MyBox
isLoading={isLoading}
display={'flex'}
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'10px'}
left={0}
pt={'20px'}
pb={4}
h={isOpen ? 'calc(100% - 20px)' : '0'}
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'0 20px 20px 0'}
transition={'.2s ease'}
userSelect={'none'}
overflow={isOpen ? 'none' : 'hidden'}
>
{/* Header */}
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
{/* Tabs */}
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<Box flex={'1 0 0'}>
<FillRowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('common:core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('common:core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('common:core.module.template.Team app'),
value: TemplateTypeEnum.teamPlugin
}
]}
width={'100%'}
py={'5px'}
value={templateType}
onChange={(e) => {
loadNodeTemplates({
type: e as TemplateTypeEnum,
parentId: ''
});
}}
/>
</Flex>
{/* Search */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) && (
<Flex mt={2} alignItems={'center'} h={10}>
<InputGroup mr={4} h={'full'}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
h={'full'}
bg={'myGray.50'}
placeholder={
templateType === TemplateTypeEnum.teamPlugin
? t('common:plugin.Search_app')
: t('common:plugin.Search plugin')
}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
<Box flex={1} />
{templateType === TemplateTypeEnum.teamPlugin && (
</Box>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
borderColor={'myGray.300'}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
/>
</Flex>
{/* Search */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) && (
<Flex mt={2} alignItems={'center'} h={10}>
<InputGroup mr={4} h={'full'}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
h={'full'}
bg={'myGray.50'}
placeholder={
templateType === TemplateTypeEnum.teamPlugin
? t('common:plugin.Search_app')
: t('common:plugin.Search plugin')
}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
<Box flex={1} />
{templateType === TemplateTypeEnum.teamPlugin && (
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/app/list')}
gap={1}
>
<Box>{t('common:create')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
{templateType === TemplateTypeEnum.systemPlugin &&
feConfigs.systemPluginCourseUrl && (
<Flex
alignItems={'center'}
cursor={'pointer'}
@@ -323,68 +339,35 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/app/list')}
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
gap={1}
>
<Box>{t('common:create')}</Box>
<Box>{t('common:plugin.contribute')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
{templateType === TemplateTypeEnum.systemPlugin &&
feConfigs.systemPluginCourseUrl && (
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
gap={1}
>
<Box>{t('common:plugin.contribute')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
</Flex>
)}
{/* paths */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) &&
!searchKey &&
parentId && (
<Flex alignItems={'center'} mt={2}>
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
</Flex>
)}
{/* paths */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) &&
!searchKey &&
parentId && (
<Flex alignItems={'center'} mt={2}>
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
</Flex>
)}
</Box>
<RenderList
templates={templates}
type={templateType}
onClose={onClose}
parentId={parentId}
setParentId={onUpdateParentId}
/>
</MyBox>
</>
);
}, [
isOpen,
onClose,
isLoading,
t,
templateType,
feConfigs.systemPluginCourseUrl,
searchKey,
parentId,
paths,
onUpdateParentId,
templates,
loadNodeTemplates,
router
]);
return Render;
</Box>
<RenderList
templates={templates}
type={templateType}
onClose={onClose}
parentId={parentId}
setParentId={onUpdateParentId}
/>
</MyBox>
</>
);
};
export default React.memo(NodeTemplatesModal);
@@ -403,9 +386,11 @@ const RenderList = React.memo(function RenderList({
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
const { screenToFlowPosition } = useReactFlow();
const { toast } = useToast();
const { reactFlowWrapper, setNodes, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const { computedNewNodeName } = useWorkflowUtils();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const formatTemplates = useMemo<NodeTemplateListType>(() => {
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
@@ -426,8 +411,6 @@ const RenderList = React.memo(function RenderList({
template: NodeTemplateListItemType;
position: XYPosition;
}) => {
if (!reactFlowWrapper?.current) return;
// Load template node
const templateNode = await (async () => {
try {
@@ -465,7 +448,8 @@ const RenderList = React.memo(function RenderList({
// Add default values to some inputs
const defaultValueMap: Record<string, any> = {
[NodeInputKeyEnum.userChatInput]: undefined
[NodeInputKeyEnum.userChatInput]: undefined,
[NodeInputKeyEnum.fileUrlList]: undefined
};
nodeList.forEach((node) => {
if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) {
@@ -473,6 +457,9 @@ const RenderList = React.memo(function RenderList({
node.nodeId,
NodeOutputKeyEnum.userChatInput
];
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
[node.nodeId, NodeOutputKeyEnum.userFiles]
];
}
});
@@ -535,16 +522,7 @@ const RenderList = React.memo(function RenderList({
return newState;
});
},
[
reactFlowWrapper,
screenToFlowPosition,
nodeList,
computedNewNodeName,
t,
setNodes,
setLoading,
toast
]
[screenToFlowPosition, nodeList, computedNewNodeName, t, setNodes, setLoading, toast]
);
const gridStyle = useMemo(() => {
@@ -567,133 +545,118 @@ const RenderList = React.memo(function RenderList({
};
}, [type]);
const Render = useMemo(() => {
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
<Box mx={'auto'}>
{formatTemplates.map((item, i) => (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
_notLast={{ mb: 5 }}
>
{item.label && formatTemplates.length > 1 && (
<Flex>
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
{t(item.label as any)}
</Box>
</Flex>
)}
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
<Box mx={'auto'}>
{formatTemplates.map((item, i) => (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
_notLast={{ mb: 5 }}
>
{item.label && formatTemplates.length > 1 && (
<Flex>
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
{t(item.label as any)}
</Box>
</Flex>
)}
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
{item.list.map((template) => (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box py={2}>
<Flex alignItems={'center'}>
<Avatar
src={template.avatar}
w={'1.75rem'}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
{t(template.name as any)}
</Box>
</Flex>
<Box mt={2} color={'myGray.500'}>
{t(template.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
</Box>
}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={!template.isFolder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (template.isFolder) {
return setParentId(template.id);
}
if (isPc) {
return onAddNode({
template,
position: { x: sliderWidth * 1.5, y: 200 }
});
}
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
onClose();
}}
>
<Avatar
src={template.avatar}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box ml={3} flex={'1'}>
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
{item.list.map((template) => (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box py={2}>
<Flex alignItems={'center'}>
<Avatar
src={template.avatar}
w={'1.75rem'}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
{t(template.name as any)}
</Box>
{gridStyle.authorInName && template.author !== undefined && (
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
{`by ${template.author || feConfigs.systemTitle}`}
</Box>
)}
</Flex>
<Box mt={2} color={'myGray.500'}>
{t(template.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{gridStyle.authorInRight && template.authorAvatar && template.author && (
<HStack spacing={1} maxW={'120px'}>
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
<Box fontSize={'xs'} className="textEllipsis">
{template.author}
</Box>
</HStack>
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
</Box>
}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={!template.isFolder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (template.isFolder) {
return setParentId(template.id);
}
if (isPc) {
return onAddNode({
template,
position: { x: sliderWidth * 1.5, y: 200 }
});
}
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
onClose();
}}
>
<Avatar
src={template.avatar}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box ml={3} flex={'1'}>
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
{t(template.name as any)}
</Box>
{gridStyle.authorInName && template.author !== undefined && (
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
{`by ${template.author || feConfigs.systemTitle}`}
</Box>
)}
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
))}
</Box>
</Box>
);
}, [
feConfigs.systemTitle,
formatTemplates,
gridStyle,
isPc,
isSystemPlugin,
onAddNode,
onClose,
setParentId,
t,
templates.length
]);
</Box>
return Render;
{gridStyle.authorInRight && template.authorAvatar && template.author && (
<HStack spacing={1} maxW={'120px'}>
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
<Box fontSize={'xs'} className="textEllipsis">
{template.author}
</Box>
</HStack>
)}
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
))}
</Box>
</Box>
);
});

View File

@@ -6,12 +6,15 @@ import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/w
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useThrottleEffect } from 'ahooks';
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
const ButtonEdge = (props: EdgeProps) => {
const { nodes, nodeList, onEdgesChange, workflowDebugData, hoverEdgeId } = useContextSelector(
WorkflowContext,
(v) => v
);
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const onEdgesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onEdgesChange);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const hoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.hoverEdgeId);
const {
id,
@@ -31,9 +34,12 @@ const ButtonEdge = (props: EdgeProps) => {
// If parentNode is folded, the edge will not be displayed
const parentNode = useMemo(() => {
return nodeList.find(
(node) => (node.nodeId === source || node.nodeId === target) && node.parentNodeId
);
for (const node of nodeList) {
if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) {
return nodeList.find((parent) => parent.nodeId === node.parentNodeId);
}
}
return undefined;
}, [nodeList, source, target]);
const defaultZIndex = useMemo(
@@ -116,13 +122,13 @@ const ButtonEdge = (props: EdgeProps) => {
(edge) => edge.sourceHandle === sourceHandleId && edge.targetHandle === targetHandleId
);
if (!targetEdge) {
if (highlightEdge) return '#3370ff';
if (highlightEdge) return '#487FFF';
return '#94B5FF';
}
// debug mode
const colorMap = {
[RuntimeEdgeStatusEnum.active]: '#39CC83',
[RuntimeEdgeStatusEnum.active]: '#487FFF',
[RuntimeEdgeStatusEnum.waiting]: '#5E8FFF',
[RuntimeEdgeStatusEnum.skipped]: '#8A95A7'
};
@@ -154,10 +160,10 @@ const ButtonEdge = (props: EdgeProps) => {
position={'absolute'}
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'17px'}
h={'17px'}
w={'18px'}
h={'18px'}
bg={'white'}
borderRadius={'17px'}
borderRadius={'18px'}
cursor={'pointer'}
zIndex={defaultZIndex + 1000}
onClick={() => onDelConnect(id)}

View File

@@ -6,10 +6,8 @@ const Container = ({ children, ...props }: BoxProps) => {
return (
<Flex
flexDirection={'column'}
px={4}
mx={2}
mb={2}
py={'10px'}
mx={3}
p={4}
position={'relative'}
bg={'myGray.50'}
border={'1px solid #F0F1F6'}

View File

@@ -7,76 +7,76 @@ import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comme
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useReactFlow } from 'reactflow';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
type ContextMenuProps = {
top: number;
left: number;
};
const ContextMenu = ({ top, left }: ContextMenuProps) => {
const ContextMenu = () => {
const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowContext, (ctx) => ctx.setNodes);
const setMenu = useContextSelector(WorkflowContext, (ctx) => ctx.setMenu);
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu);
const { screenToFlowPosition } = useReactFlow();
const newNode = nodeTemplate2FlowNode({
template: CommentNode,
position: screenToFlowPosition({ x: left, y: top }),
position: screenToFlowPosition({ x: menu?.left ?? 0, y: menu?.top ?? 0 }),
t
});
return (
<Box position="relative">
<Box
position="absolute"
top={`${top - 6}px`}
left={`${left + 10}px`}
width={0}
height={0}
borderLeft="6px solid transparent"
borderRight="6px solid transparent"
borderBottom="6px solid white"
zIndex={2}
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
/>
<Flex
position={'absolute'}
top={top}
left={left}
bg={'white'}
w={'120px'}
height={9}
p={1}
rounded={'md'}
boxShadow={'0px 2px 4px 0px #A1A7B340'}
className="context-menu"
alignItems={'center'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
setMenu(null);
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNode);
return newState;
});
}}
zIndex={1}
>
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{t('workflow:context_menu.add_comment')}
</Box>
</Flex>
</Box>
!!menu && (
<Box position="relative">
<Box
position="absolute"
top={`${menu.top - 6}px`}
left={`${menu.left + 10}px`}
width={0}
height={0}
borderLeft="6px solid transparent"
borderRight="6px solid transparent"
borderBottom="6px solid white"
zIndex={2}
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
/>
<Flex
position={'absolute'}
top={menu.top}
left={menu.left}
bg={'white'}
w={'120px'}
height={9}
p={1}
rounded={'md'}
boxShadow={'0px 2px 4px 0px #A1A7B340'}
className="context-menu"
alignItems={'center'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
setMenu(null);
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNode);
return newState;
});
}}
zIndex={1}
>
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{t('workflow:context_menu.add_comment')}
</Box>
</Flex>
</Box>
)
);
};

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import {
Background,
ControlButton,
@@ -15,8 +15,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import styles from './index.module.scss';
import { maxZoom, minZoom } from '../index';
import { maxZoom, minZoom } from '../../constants';
import { useKeyPress } from 'ahooks';
import { WorkflowEventContext } from '../../context/workflowEventContext';
const buttonStyle = {
border: 'none',
@@ -27,31 +28,41 @@ const buttonStyle = {
const FlowController = React.memo(function FlowController() {
const { fitView, zoomIn, zoomOut } = useReactFlow();
const { zoom } = useViewport();
const {
undo,
redo,
canRedo,
canUndo,
workflowControlMode,
setWorkflowControlMode,
mouseInCanvas,
nodeList
} = useContextSelector(WorkflowContext, (v) => v);
const undo = useContextSelector(WorkflowContext, (v) => v.undo);
const redo = useContextSelector(WorkflowContext, (v) => v.redo);
const canRedo = useContextSelector(WorkflowContext, (v) => v.canRedo);
const canUndo = useContextSelector(WorkflowContext, (v) => v.canUndo);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const workflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.workflowControlMode
);
const setWorkflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.setWorkflowControlMode
);
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
const { t } = useTranslation();
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
// Controller shortcut key
useKeyPress(['ctrl.z', 'meta.z'], (e) => {
useKeyPress(['ctrl.z', 'meta.z', 'ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
e.preventDefault();
e.stopPropagation();
if (!mouseInCanvas) return;
undo();
});
useKeyPress(['ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
if (!mouseInCanvas) return;
redo();
const isRedo = (e.key.toLowerCase() === 'z' && e.shiftKey) || e.key.toLowerCase() === 'y';
if (isRedo) {
redo();
} else {
undo();
}
});
useKeyPress(['ctrl.add', 'meta.add', 'ctrl.equalsign', 'meta.equalsign'], (e) => {
e.preventDefault();
e.stopPropagation();
if (!mouseInCanvas) return;
zoomIn();
});

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { Box, StackProps, HStack } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => {
return (
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={3} {...props}>
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={4} {...props}>
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} />
<Box color={'myGray.900'}>{text}</Box>
</HStack>

View File

@@ -1,7 +1,7 @@
import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useCallback, useState, useMemo, useEffect } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { checkWorkflowNodeAndConnection } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -27,13 +27,14 @@ import {
} from '@fastgpt/global/core/workflow/constants';
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../../context';
import { WorkflowContext } from '../../context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '../../../context';
import { VariableInputItem } from '@/components/core/chat/ChatContainer/ChatBox/components/VariableInput';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
const MyRightDrawer = dynamic(
() => import('@fastgpt/web/components/common/MyDrawer/MyRightDrawer')
@@ -49,9 +50,10 @@ export const useDebug = () => {
const { t } = useTranslation();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const getNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.getNodes);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const onStartNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStartNodeDebug);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
@@ -76,7 +78,7 @@ export const useDebug = () => {
const [runtimeEdges, setRuntimeEdges] = useState<RuntimeEdgeItemType[]>();
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
const nodes = getNodes();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {

View File

@@ -5,14 +5,18 @@ import { useTranslation } from 'next-i18next';
import { Node, useKeyPress } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
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';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
export const useKeyboard = () => {
const { t } = useTranslation();
const { setNodes, mouseInCanvas } = useContextSelector(WorkflowContext, (v) => v);
const getNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.getNodes);
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
const { copyData } = useCopyData();
const { computedNewNodeName } = useWorkflowUtils();
@@ -33,14 +37,14 @@ export const useKeyboard = () => {
const onCopy = useCallback(async () => {
if (hasInputtingElement()) return;
const { nodes } = await getWorkflowStore();
const nodes = getNodes();
const selectedNodes = nodes.filter(
(node) => node.selected && !node.data?.isError && node.data?.unique !== true
);
if (selectedNodes.length === 0) return;
copyData(JSON.stringify(selectedNodes), t('common:core.workflow.Copy node'));
}, [copyData, hasInputtingElement, t]);
}, [copyData, getNodes, hasInputtingElement, t]);
const onParse = useCallback(async () => {
if (hasInputtingElement()) return;

View File

@@ -24,13 +24,17 @@ 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 { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useDebounceEffect, 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';
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../../context/workflowInitContext';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { AppContext } from '../../../context';
import { WorkflowEventContext } from '../../context/workflowEventContext';
/*
Compute helper lines for snapping nodes to each other
@@ -271,26 +275,48 @@ export const useWorkflow = () => {
const { toast } = useToast();
const { t } = useTranslation();
const { isDowningCtrl } = useKeyboard();
const {
setConnectingEdge,
edges,
nodes,
nodeList,
onNodesChange,
setEdges,
onChangeNode,
onEdgesChange,
setHoverEdgeId,
setMenu
} = useContextSelector(WorkflowContext, (v) => v);
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
const nodes = useContextSelector(WorkflowInitContext, (state) => state.nodes);
const onNodesChange = useContextSelector(WorkflowNodeEdgeContext, (state) => state.onNodesChange);
const edges = useContextSelector(WorkflowNodeEdgeContext, (state) => state.edges);
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
const onEdgesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onEdgesChange);
const { setConnectingEdge, nodeList, onChangeNode, pushPastSnapshot } = useContextSelector(
WorkflowContext,
(v) => v
);
const setHoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverEdgeId);
const setMenu = useContextSelector(WorkflowEventContext, (v) => v.setMenu);
const { getIntersectingNodes } = useReactFlow();
const { isDowningCtrl } = useKeyboard();
// 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;
const resetParentNodeSizeAndPosition = useMemoizedFn((parentId: string) => {
const { childNodes, loopNode } = nodes.reduce(
(acc, node) => {
if (node.data.parentNodeId === parentId) {
acc.childNodes.push(node);
}
if (node.id === parentId) {
acc.loopNode = node;
}
return acc;
},
{ childNodes: [] as Node[], loopNode: undefined as Node<FlowNodeItemType> | undefined }
);
if (!loopNode) return;
const rect = getNodesBounds(childNodes);
// Calculate parent node size with minimum width/height constraints
const width = Math.max(rect.width + 80, 840);
const height = Math.max(rect.height + 80, 600);
const offsetHeight =
loopNode.data.inputs.find((input) => input.key === NodeInputKeyEnum.loopNodeInputHeight)
?.value ?? 83;
// Update parentNode size and position
onChangeNode({
@@ -311,15 +337,14 @@ export const useWorkflow = () => {
value: height
}
});
// Update parentNode position
onNodesChange([
{
id: parentId,
type: 'position',
position: {
x: rect.x - 50,
y: rect.y - 280
x: rect.x - 70,
y: rect.y - offsetHeight - 240
}
}
]);
@@ -369,15 +394,6 @@ export const useWorkflow = () => {
// 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.data.isFolded && item.type === FlowNodeTypeEnum.loop
);
const unSupportedTypes = [
FlowNodeTypeEnum.workflowStart,
FlowNodeTypeEnum.loop,
@@ -386,7 +402,16 @@ export const useWorkflow = () => {
FlowNodeTypeEnum.systemConfig
];
if (parentNode && !node.data.parentNodeId) {
if (!node || node.data.parentNodeId) return;
// 获取所有与当前节点相交的节点
const intersections = getIntersectingNodes(node);
// 获取所有与当前节点相交的节点中,类型为 loop 的节点且它不能是折叠状态
const parentNode = intersections.find(
(item) => !item.data.isFolded && item.type === FlowNodeTypeEnum.loop
);
if (parentNode) {
if (unSupportedTypes.includes(node.type as FlowNodeTypeEnum)) {
return toast({
status: 'warning',
@@ -404,10 +429,6 @@ export const useWorkflow = () => {
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);
}
});
@@ -462,7 +483,7 @@ export const useWorkflow = () => {
const childNodes = nodes.filter((n) => n.data.parentNodeId === parentId);
checkNodeHelpLine(change, childNodes);
resetParentNodeSizeAndPosition(getNodesBounds(childNodes), parentId);
resetParentNodeSizeAndPosition(parentId);
}
// If node is parent node, move parent node and child nodes
else if (parentNode[node.data.flowNodeType]) {
@@ -591,8 +612,36 @@ export const useWorkflow = () => {
state
)
);
// Add default input
const node = nodeList.find((n) => n.nodeId === connect.target);
if (!node) return;
// 1. Add file input
if (
node.flowNodeType === FlowNodeTypeEnum.chatNode ||
node.flowNodeType === FlowNodeTypeEnum.tools ||
node.flowNodeType === FlowNodeTypeEnum.appModule
) {
const input = node.inputs.find((i) => i.key === NodeInputKeyEnum.fileUrlList);
if (input && (!input?.value || input.value.length === 0)) {
const workflowStartNode = nodeList.find(
(n) => n.flowNodeType === FlowNodeTypeEnum.workflowStart
);
if (!workflowStartNode) return;
onChangeNode({
nodeId: node.nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.fileUrlList,
value: {
...input,
value: [[workflowStartNode.nodeId, NodeOutputKeyEnum.userFiles]]
}
});
}
}
},
[setEdges]
[nodeList, onChangeNode, setEdges]
);
const customOnConnect = useCallback(
(connect: Connection) => {
@@ -642,6 +691,23 @@ export const useWorkflow = () => {
setMenu(null);
}, [setMenu]);
// Watch
// Auto save snapshot
useDebounceEffect(
() => {
if (nodes.length === 0 || !appDetail.chatConfig) return;
pushPastSnapshot({
pastNodes: nodes,
pastEdges: edges,
customTitle: formatTime2YMDHMS(new Date()),
chatConfig: appDetail.chatConfig
});
},
[nodes, edges, appDetail.chatConfig],
{ wait: 500 }
);
return {
handleNodesChange,
handleEdgeChange,
@@ -655,7 +721,8 @@ export const useWorkflow = () => {
helperLineVertical,
onNodeDragStop,
onPaneContextMenu,
onPaneClick
onPaneClick,
resetParentNodeSizeAndPosition
};
};

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ReactFlow, { NodeProps, ReactFlowProvider, SelectionMode } from 'reactflow';
import ReactFlow, { NodeProps, SelectionMode } from 'reactflow';
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
@@ -11,16 +11,14 @@ import NodeTemplatesModal from './NodeTemplatesModal';
import 'reactflow/dist/style.css';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
import { connectionLineStyle, defaultEdgeOptions, maxZoom, minZoom } from '../constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useWorkflow } from './hooks/useWorkflow';
import HelperLines from './components/HelperLines';
import FlowController from './components/FlowController';
import ContextMenu from './components/ContextMenu';
export const minZoom = 0.1;
export const maxZoom = 1.5;
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../context/workflowInitContext';
import { WorkflowEventContext } from '../context/workflowEventContext';
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
@@ -66,9 +64,12 @@ const edgeTypes = {
};
const Workflow = () => {
const { nodes, edges, menu, reactFlowWrapper, workflowControlMode } = useContextSelector(
WorkflowContext,
(v) => v
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const reactFlowWrapper = useContextSelector(WorkflowEventContext, (v) => v.reactFlowWrapper);
const workflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.workflowControlMode
);
const {
@@ -125,7 +126,7 @@ const Workflow = () => {
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</>
{menu && <ContextMenu {...menu} />}
<ContextMenu />
<ReactFlow
ref={reactFlowWrapper}
fitView
@@ -169,12 +170,4 @@ const Workflow = () => {
);
};
const Render = () => {
return (
<ReactFlowProvider>
<Workflow />
</ReactFlowProvider>
);
};
export default React.memo(Render);
export default React.memo(Workflow);

View File

@@ -5,7 +5,7 @@
*/
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import { Background, NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import Container from '../../components/Container';
@@ -15,28 +15,101 @@ 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 {
ArrayTypeMap,
NodeInputKeyEnum,
VARIABLE_NODE_ID,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import {
Input_Template_Children_Node_List,
Input_Template_LOOP_NODE_OFFSET
} from '@fastgpt/global/core/workflow/template/input';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { AppContext } from '../../../../context';
import { isValidArrayReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { ReferenceArrayValueType } from '@fastgpt/global/core/workflow/type/io';
import { useWorkflow } from '../../hooks/useWorkflow';
import { useSize } from 'ahooks';
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, inputs, outputs, isFolded } = data;
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const { nodeWidth, nodeHeight } = useMemo(() => {
const { resetParentNodeSizeAndPosition } = useWorkflow();
const {
nodeWidth,
nodeHeight,
loopInputArray,
loopNodeInputHeight = Input_Template_LOOP_NODE_OFFSET
} = useMemo(() => {
return {
nodeWidth: inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value,
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value
nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value,
loopInputArray: inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray),
loopNodeInputHeight: inputs.find(
(input) => input.key === NodeInputKeyEnum.loopNodeInputHeight
)
};
}, [inputs]);
// Update array input type
// Computed the reference value type
const newValueType = useMemo(() => {
if (!loopInputArray) return WorkflowIOValueTypeEnum.arrayAny;
const value = loopInputArray.value as ReferenceArrayValueType;
if (
!value ||
value.length === 0 ||
!isValidArrayReferenceValue(
value,
nodeList.map((node) => node.nodeId)
)
)
return WorkflowIOValueTypeEnum.arrayAny;
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig
});
const valueType = ((value) => {
if (value?.[0] === VARIABLE_NODE_ID) {
return globalVariables.find((item) => item.key === value[1])?.valueType;
} else {
const node = nodeList.find((node) => node.nodeId === value?.[0]);
const output = node?.outputs.find((output) => output.id === value?.[1]);
return output?.valueType;
}
})(value[0]);
return ArrayTypeMap[valueType as keyof typeof ArrayTypeMap] ?? WorkflowIOValueTypeEnum.arrayAny;
}, [appDetail.chatConfig, loopInputArray, nodeList]);
useEffect(() => {
if (!loopInputArray) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.loopInputArray,
value: {
...loopInputArray,
valueType: newValueType
}
});
}, [newValueType]);
// Update childrenNodeIdList
const childrenNodeIdList = useMemo(() => {
return JSON.stringify(
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
);
}, [nodeId, nodeList]);
useEffect(() => {
onChangeNode({
nodeId,
@@ -47,31 +120,52 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
value: JSON.parse(childrenNodeIdList)
}
});
resetParentNodeSizeAndPosition(nodeId);
}, [childrenNodeIdList]);
// Update loop node offset value
const inputBoxRef = useRef<HTMLDivElement>(null);
const size = useSize(inputBoxRef);
useEffect(() => {
if (!size?.height) return;
onChangeNode({
nodeId,
type: 'replaceInput',
key: NodeInputKeyEnum.loopNodeInputHeight,
value: {
...loopNodeInputHeight,
value: size.height
}
});
setTimeout(() => {
resetParentNodeSizeAndPosition(nodeId);
}, 50);
}, [size?.height]);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
maxW="full"
{...(!isFolded && {
minW: '900px',
minH: '900px',
w: nodeWidth,
h: nodeHeight
})}
menuForbid={{ copy: true }}
{...data}
>
<NodeCard selected={selected} maxW="full" menuForbid={{ copy: true }} {...data}>
<Container position={'relative'} flex={1}>
<IOTitle text={t('common:common.Input')} />
<Box mb={6} maxW={'360'}>
<Box mb={6} maxW={'500px'} ref={inputBoxRef}>
<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'}>
<Box
flex={1}
position={'relative'}
border={'base'}
bg={'myGray.100'}
rounded={'8px'}
{...(!isFolded && {
minW: nodeWidth,
minH: nodeHeight
})}
>
<Background />
</Box>
</Container>
@@ -80,7 +174,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
</Container>
</NodeCard>
);
}, [selected, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
}, [selected, isFolded, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
return Render;
};

View File

@@ -80,7 +80,7 @@ const NodeLoopEnd = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
debug: true
}}
>
<Box px={4} pb={4}>
<Box px={4} pb={4} pt={2}>
{inputItem && <Reference item={inputItem} nodeId={nodeId} />}
</Box>
</NodeCard>

View File

@@ -8,24 +8,27 @@ import { WorkflowContext } from '../../../context';
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
toolValueTypeList,
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 {
FlowNodeOutputTypeEnum,
FlowValueTypeMap
} 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
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object
};
const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId } = data;
const { nodeId, outputs } = data;
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const loopStartNode = useMemo(
@@ -39,12 +42,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
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;
return typeMap[parentArrayInput?.valueType as keyof typeof typeMap];
}, [loopStartNode?.parentNodeId, nodeList]);
// Auth update loopStartInput output
@@ -71,7 +69,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
key: NodeOutputKeyEnum.loopStartInput,
label: t('workflow:Array_element'),
type: FlowNodeOutputTypeEnum.static,
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
valueType: loopItemInputType
}
});
}
@@ -83,7 +81,7 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
key: NodeOutputKeyEnum.loopStartInput,
value: {
...loopArrayOutput,
valueType: typeMap[loopItemInputType as keyof typeof typeMap]
valueType: loopItemInputType
}
});
}
@@ -100,23 +98,21 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
debug: true
}}
>
<Box px={4} w={'420px'} h={'116px'}>
{!loopItemInputType ? (
<EmptyTip text={t('workflow:loop_start_tip')} py={0} mt={4} iconSize={'32px'} />
) : (
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('workflow:Variable_name')}
</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Box px={4} pt={2} w={'420px'}>
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('workflow:Variable_name')}
</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
</Tr>
</Thead>
<Tbody>
{outputs.map((output) => (
<Tr key={output.id}>
<Td>
<Flex alignItems={'center'}>
<MyIcon
@@ -125,20 +121,20 @@ const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
mr={1}
color={'primary.600'}
/>
{t('workflow:Array_element')}
{t(output.label as any)}
</Flex>
</Td>
<Td>{typeMap[loopItemInputType]}</Td>
{output.valueType && <Td>{FlowValueTypeMap[output.valueType]?.label}</Td>}
</Tr>
</Tbody>
</Table>
</TableContainer>
</Box>
)}
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</Box>
</NodeCard>
);
}, [data, loopItemInputType, selected, t]);
}, [data, outputs, selected, t]);
return Render;
};

View File

@@ -16,7 +16,7 @@ const NodeAnswer = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { isTool, commonInputs } = splitToolInputs(inputs, nodeId);
return (
<NodeCard minW={'400px'} selected={selected} {...data}>
<NodeCard selected={selected} {...data}>
<Container>
{isTool && (
<>

View File

@@ -99,7 +99,7 @@ const NodeCQNode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', item.key)}
position={Position.Right}
translate={[26, 0]}
translate={[34, 0]}
/>
</Box>
</Box>

View File

@@ -7,14 +7,13 @@ import RenderInput from './render/RenderInput';
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import { NodeInputKeyEnum, VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getOneQuoteInputTemplate } from '@fastgpt/global/core/workflow/template/system/datasetConcat';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MySlider from '@/components/Slider';
import {
FlowNodeInputItemType,
ReferenceValueProps
ReferenceItemValueType
} from '@fastgpt/global/core/workflow/type/io.d';
import RenderOutput from './render/RenderOutput';
import IOTitle from '../components/IOTitle';
@@ -24,94 +23,13 @@ import { ReferSelector, useReference } from './render/RenderInput/templates/Refe
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
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, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const Reference = useMemoizedFn(
({ 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}
/>
</>
);
}
);
const CustomComponent = useMemo(() => {
const quoteList = inputs.filter((item) => item.canEdit);
const tokenLimit = (() => {
@@ -184,7 +102,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<Box mt={2}>
{quoteList.map((children) => (
<Box key={children.key} _notLast={{ mb: 3 }}>
<Reference nodeId={nodeId} inputChildren={children} />
<VariableSelector nodeId={nodeId} inputChildren={children} />
</Box>
))}
</Box>
@@ -192,7 +110,7 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
);
}
};
}, [Reference, inputs, nodeId, nodeList, onChangeNode, t, llmModelList]);
}, [inputs, nodeId, nodeList, onChangeNode, t]);
const Render = useMemo(() => {
return (
@@ -212,3 +130,75 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return Render;
};
export default React.memo(NodeDatasetConcat);
const VariableSelector = ({
nodeId,
inputChildren
}: {
nodeId: string;
inputChildren: FlowNodeInputItemType;
}) => {
const { t } = useTranslation();
const { onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const { referenceList } = useReference({
nodeId,
valueType: inputChildren.valueType
});
const onSelect = useCallback(
(e?: ReferenceItemValueType) => {
if (!e) return;
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value: e
}
});
},
[inputChildren, nodeId, 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={inputChildren.value}
onSelect={onSelect}
isArray={false}
/>
</>
);
};

View File

@@ -60,7 +60,8 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
</Box>
<Button
size={'sm'}
variant={'whitePrimary'}
variant={'ghost'}
color={'myGray.600'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() => setEditExtractField(defaultField)}
>
@@ -78,12 +79,10 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
<Table bg={'white'}>
<Thead>
<Tr>
<Th bg={'myGray.50'} borderRadius={'none !important'}>
{t('common:item_name')}
</Th>
<Th bg={'myGray.50'}>{t('common:item_description')}</Th>
<Th bg={'myGray.50'}>{t('common:required')}</Th>
<Th bg={'myGray.50'} borderRadius={'none !important'}></Th>
<Th borderRadius={'none !important'}>{t('common:item_name')}</Th>
<Th>{t('common:item_description')}</Th>
<Th>{t('common:required')}</Th>
<Th borderRadius={'none !important'}></Th>
</Tr>
</Thead>
<Tbody>

View File

@@ -46,11 +46,6 @@ const InputFormEditModal = ({
const inputType = watch('type') || FlowNodeInputTypeEnum.input;
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const defaultInputValue = watch('defaultValue');
const inputTypeList = [
{
icon: 'core/workflow/inputType/input',
@@ -187,14 +182,9 @@ const InputFormEditModal = ({
type={'formInput'}
isEdit={isEdit}
inputType={inputType}
maxLength={maxLength}
max={max}
min={min}
defaultValue={defaultInputValue}
onClose={onClose}
onSubmitSuccess={onSubmitSuccess}
onSubmitError={onSubmitError}
valueType={defaultValueType}
/>
</Flex>
</MyModal>

View File

@@ -49,6 +49,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getEditorVariables } from '../../../utils';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
const defaultFormBody = {
@@ -80,9 +81,10 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
}) {
const { t } = useTranslation();
const { toast } = useToast();
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
@@ -256,8 +258,9 @@ export function RenderHttpProps({
}) {
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { appDetail } = useContextSelector(AppContext, (v) => v);

View File

@@ -7,7 +7,7 @@ import Container from '../../components/Container';
import { MinusIcon, SmallAddIcon } from '@chakra-ui/icons';
import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferenceItemValueType } from '@fastgpt/global/core/workflow/type/io';
import { useTranslation } from 'next-i18next';
import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
@@ -62,6 +62,7 @@ const ListItem = ({
position={'relative'}
transform={snapshot.isDragging ? `scale(${getZoom()})` : ''}
transformOrigin={'top left'}
mb={2}
>
<Container w={snapshot.isDragging ? '' : 'full'} className="nodrag">
<Flex mb={4} alignItems={'center'}>
@@ -122,7 +123,7 @@ const ListItem = ({
<Flex gap={2} mb={2} alignItems={'center'}>
{/* variable reference */}
<Box minW={'250px'}>
<Reference
<VariableSelector
nodeId={nodeId}
variable={item.variable}
onSelect={(e) => {
@@ -255,7 +256,6 @@ const ListItem = ({
}}
variant={'link'}
leftIcon={<SmallAddIcon />}
my={3}
color={'primary.600'}
>
{t('common:core.module.input.add')}
@@ -266,7 +266,7 @@ const ListItem = ({
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[18, 0]}
translate={[5, 0]}
/>
)}
</Flex>
@@ -302,29 +302,29 @@ const ListItem = ({
export default React.memo(ListItem);
const Reference = ({
const VariableSelector = ({
nodeId,
variable,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
onSelect: (e: ReferenceValueProps) => void;
variable?: ReferenceItemValueType;
onSelect: (e?: ReferenceItemValueType) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
const { referenceList } = useReference({
nodeId,
valueType: WorkflowIOValueTypeEnum.any,
value: variable
valueType: WorkflowIOValueTypeEnum.any
});
return (
<ReferSelector
placeholder={t('common:select_reference_variable')}
list={referenceList}
value={formatValue}
value={variable}
onSelect={onSelect}
isArray={false}
/>
);
};
@@ -336,7 +336,7 @@ const ConditionSelect = ({
onSelect
}: {
condition?: VariableConditionEnum;
variable?: ReferenceValueProps;
variable?: ReferenceItemValueType;
onSelect: (e: VariableConditionEnum) => void;
}) => {
const { t } = useTranslation();
@@ -414,7 +414,7 @@ const ConditionValueInput = ({
onChange
}: {
value?: string;
variable?: ReferenceValueProps;
variable?: ReferenceItemValueType;
condition?: VariableConditionEnum;
onChange: (e: string) => void;
}) => {

View File

@@ -48,7 +48,7 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} cursor={'default'}>
<Flex flexDirection={'column'} cursor={'default'}>
<DndDrag<IfElseListItemType>
onDragEndCb={(list: IfElseListItemType[]) => onUpdateIfElseList(list)}
dataList={ifElseList}
@@ -98,12 +98,12 @@ const NodeIfElse = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
nodeId={nodeId}
handleId={elseHandleId}
position={Position.Right}
translate={[26, 0]}
translate={[18, 0]}
/>
</Flex>
</Container>
</Box>
<Box py={3} px={6}>
</Flex>
<Box py={3} px={4}>
<Button
variant={'whiteBase'}
w={'full'}

View File

@@ -11,7 +11,6 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useBoolean } from 'ahooks';
import InputTypeConfig from './InputTypeConfig';
export const defaultInput: FlowNodeInputItemType = {
@@ -23,7 +22,10 @@ export const defaultInput: FlowNodeInputItemType = {
label: '',
description: '',
defaultValue: '',
list: [{ label: '', value: '' }]
list: [{ label: '', value: '' }],
maxFiles: 5,
canSelectFile: true,
canSelectImg: true
};
const FieldEditModal = ({
@@ -108,6 +110,13 @@ const FieldEditModal = ({
])
],
[
{
icon: 'core/workflow/inputType/file',
label: t('app:file_upload'),
value: [FlowNodeInputTypeEnum.fileSelect],
defaultValueType: WorkflowIOValueTypeEnum.arrayString,
description: t('app:file_upload_tip')
},
{
icon: 'core/workflow/inputType/customVariable',
label: t('common:core.workflow.inputType.custom'),
@@ -130,19 +139,10 @@ const FieldEditModal = ({
const form = useForm({
defaultValues: defaultValue
});
const { getValues, setValue, watch, reset } = form;
const { setValue, watch, reset } = form;
const renderTypeList = watch('renderTypeList');
const inputType = renderTypeList[0] || FlowNodeInputTypeEnum.reference;
const valueType = watch('valueType');
const [isToolInput, { toggle: setIsToolInput }] = useBoolean(!!getValues('toolDescription'));
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
const defaultInputValue = watch('defaultValue');
const defaultValueType = useMemo(
() =>
@@ -190,8 +190,8 @@ const FieldEditModal = ({
}
}
// Focus remove toolDescription
if (isToolInput && data.renderTypeList.includes(FlowNodeInputTypeEnum.reference)) {
// Get toolDescription and removes the types of some unusable tools
if (data.toolDescription && data.renderTypeList.includes(FlowNodeInputTypeEnum.reference)) {
data.toolDescription = data.description;
} else {
data.toolDescription = undefined;
@@ -211,18 +211,7 @@ const FieldEditModal = ({
reset(defaultInput);
}
},
[
defaultValue.key,
defaultValueType,
isEdit,
isToolInput,
keys,
onSubmit,
t,
toast,
onClose,
reset
]
[defaultValue.key, defaultValueType, isEdit, keys, onSubmit, t, toast, onClose, reset]
);
const onSubmitError = useCallback(
(e: Object) => {
@@ -241,7 +230,7 @@ const FieldEditModal = ({
return (
<MyModal
isOpen={true}
isOpen
onClose={onClose}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? t('workflow:edit_input') : t('workflow:add_new_input')}
@@ -321,14 +310,6 @@ const FieldEditModal = ({
isEdit={isEdit}
onClose={onClose}
inputType={inputType}
maxLength={maxLength}
max={max}
min={min}
selectValueTypeList={selectValueTypeList}
defaultValue={defaultInputValue}
isToolInput={isToolInput}
setIsToolInput={setIsToolInput}
valueType={valueType}
defaultValueType={defaultValueType}
onSubmitSuccess={onSubmitSuccess}
onSubmitError={onSubmitError}

View File

@@ -3,7 +3,6 @@ import {
Button,
Flex,
FormControl,
FormLabel,
HStack,
Input,
NumberDecrementStepper,
@@ -23,8 +22,6 @@ import {
FlowNodeInputTypeEnum,
FlowValueTypeMap
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
@@ -35,8 +32,12 @@ import { useTranslation } from 'react-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
type ListValueType = { id: string; value: string; label: string }[];
import ChatFunctionTip from '@/components/core/app/Tip';
import MySlider from '@/components/Slider';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const InputTypeConfig = ({
form,
@@ -44,36 +45,18 @@ const InputTypeConfig = ({
onClose,
type,
inputType,
maxLength,
max,
min,
selectValueTypeList,
defaultValue,
isToolInput,
setIsToolInput,
valueType,
defaultValueType,
onSubmitSuccess,
onSubmitError
}: {
// Common fields
form: UseFormReturn<any>;
form: UseFormReturn<any, any>;
isEdit: boolean;
onClose: () => void;
type: 'plugin' | 'formInput' | 'variable';
inputType: FlowNodeInputTypeEnum | VariableInputEnum;
maxLength?: number;
max?: number;
min?: number;
selectValueTypeList?: WorkflowIOValueTypeEnum[];
defaultValue?: string;
// Plugin-specific fields
isToolInput?: boolean;
setIsToolInput?: () => void;
valueType?: WorkflowIOValueTypeEnum;
defaultValueType?: WorkflowIOValueTypeEnum;
// Update methods
@@ -82,9 +65,7 @@ const InputTypeConfig = ({
}) => {
const { t } = useTranslation();
const defaultListValue = { label: t('common:None'), value: '' };
const { register, setValue, handleSubmit, control, watch } = form;
const listValue: ListValueType = watch('list');
const { feConfigs } = useSystemStore();
const typeLabels = {
name: {
@@ -99,6 +80,18 @@ const InputTypeConfig = ({
}
};
const { register, setValue, handleSubmit, control, watch } = form;
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
const defaultValue = watch('defaultValue');
const valueType = watch('valueType');
const toolDescription = watch('toolDescription');
const isToolInput = !!toolDescription;
const listValue = watch('list') ?? [];
const {
fields: selectEnums,
append: appendEnums,
@@ -166,6 +159,10 @@ const InputTypeConfig = ({
return type === 'plugin' && list.includes(inputType as FlowNodeInputTypeEnum);
}, [inputType, type]);
// File select
const maxFiles = watch('maxFiles');
const maxSelectFiles = Math.min(feConfigs?.uploadFileMaxAmount ?? 20, 50);
return (
<Stack flex={1} borderLeft={'1px solid #F0F1F6'} justifyContent={'space-between'}>
<Flex flexDirection={'column'} p={8} pb={2} gap={4} flex={'1 0 0'} overflow={'auto'}>
@@ -175,6 +172,7 @@ const InputTypeConfig = ({
</FormLabel>
<Input
bg={'myGray.50'}
maxLength={30}
placeholder="appointment/sql"
{...register('label', {
required: true
@@ -189,7 +187,9 @@ const InputTypeConfig = ({
bg={'myGray.50'}
placeholder={t('workflow:field_description_placeholder')}
rows={3}
{...register('description', { required: isToolInput ? true : false })}
{...register('description', {
required: showIsToolInput && isToolInput ? true : false
})}
/>
</Flex>
@@ -213,7 +213,7 @@ const InputTypeConfig = ({
</Box>
) : (
<Box fontSize={'14px'} mb={2}>
{defaultValueType}
{defaultValueType ? t(FlowValueTypeMap[defaultValueType]?.label as any) : ''}
</Box>
)}
</Flex>
@@ -236,7 +236,7 @@ const InputTypeConfig = ({
<Switch
isChecked={isToolInput}
onChange={(e) => {
setIsToolInput && setIsToolInput();
setValue('toolDescription', e.target.checked ? 'sign' : '');
}}
/>
</Flex>
@@ -249,8 +249,6 @@ const InputTypeConfig = ({
{t('common:core.module.Max Length')}
</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
placeholder={t('common:core.module.Max Length placeholder')}
value={maxLength}
max={50000}
@@ -269,8 +267,6 @@ const InputTypeConfig = ({
{t('common:core.module.Max Value')}
</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={max}
onChange={(e) => {
// @ts-ignore
@@ -283,8 +279,6 @@ const InputTypeConfig = ({
{t('common:core.module.Min Value')}
</FormLabel>
<MyNumberInput
flex={'1 0 0'}
bg={'myGray.50'}
value={min}
onChange={(e) => {
// @ts-ignore
@@ -302,18 +296,15 @@ const InputTypeConfig = ({
</FormLabel>
<Flex alignItems={'start'} flex={1} h={10}>
{inputType === FlowNodeInputTypeEnum.numberInput && (
<NumberInput flex={1} step={1} min={min} max={max} position={'relative'}>
<NumberInputField
{...register('defaultValue', {
min: min,
max: max
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<MyNumberInput
value={defaultValue}
min={min}
max={max}
onChange={(e) => {
// @ts-ignore
setValue('defaultValue', e || '');
}}
/>
)}
{inputType === FlowNodeInputTypeEnum.input && (
<MyTextarea
@@ -347,7 +338,7 @@ const InputTypeConfig = ({
value: item.value
}))}
value={
defaultValue && listValue.map((item) => item.value).includes(defaultValue)
defaultValue && listValue.map((item: any) => item.value).includes(defaultValue)
? defaultValue
: ''
}
@@ -363,12 +354,12 @@ const InputTypeConfig = ({
{inputType === FlowNodeInputTypeEnum.addInputParam && (
<>
<Flex alignItems={'center'}>
{/* <Flex alignItems={'center'}>
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
{t('common:core.module.Input Type')}
</FormLabel>
<Box fontSize={'14px'}>{t('workflow:only_the_reference_type_is_supported')}</Box>
</Flex>
</Flex> */}
<Box>
<HStack mb={1}>
<FormLabel fontWeight={'medium'}>{t('workflow:optional_value_type')}</FormLabel>
@@ -395,7 +386,9 @@ const InputTypeConfig = ({
.map((id) => mergedSelectEnums.find((item) => item.id === id))
.filter(Boolean) as { id: string; value: string }[];
removeEnums();
newSelectEnums.forEach((item) => appendEnums(item));
newSelectEnums.forEach((item) =>
appendEnums({ label: item.value, value: item.value })
);
// 防止最后一个元素被focus
setTimeout(() => {
@@ -511,6 +504,60 @@ const InputTypeConfig = ({
</Button>
</>
)}
{inputType === FlowNodeInputTypeEnum.fileSelect && (
<>
<Flex alignItems={'center'} minH={'40px'}>
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
{t('app:document_upload')}
</FormLabel>
<Switch
{...register('canSelectFile', {
required: true
})}
/>
</Flex>
<Box w={'full'} minH={'40px'}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
{t('app:image_upload')}
</FormLabel>
<Switch
{...register('canSelectImg', {
required: true
})}
/>
</Flex>
<Flex color={'myGray.500'}>
<Box fontSize={'xs'}>{t('app:image_upload_tip')}</Box>
<ChatFunctionTip type="visionModel" />
</Flex>
</Box>
<Box>
<HStack>
<FormLabel fontWeight={'medium'}>{t('app:upload_file_max_amount')}</FormLabel>
<QuestionTip label={t('app:upload_file_max_amount_tip')} />
</HStack>
<Box mt={5}>
<MySlider
markList={[
{ label: '1', value: 1 },
{ label: `${maxSelectFiles}`, value: maxSelectFiles }
]}
width={'100%'}
min={1}
max={maxSelectFiles}
step={1}
value={maxFiles ?? 5}
onChange={(e) => {
setValue('maxFiles', e);
}}
/>
</Box>
</Box>
</>
)}
</Flex>
<Flex justify={'flex-end'} gap={3} pb={8} pr={8}>
@@ -520,10 +567,7 @@ const InputTypeConfig = ({
<Button
variant={'primaryOutline'}
fontWeight={'medium'}
onClick={handleSubmit(
(data: FlowNodeInputItemType) => onSubmitSuccess(data, 'confirm'),
onSubmitError
)}
onClick={handleSubmit((data) => onSubmitSuccess(data, 'confirm'), onSubmitError)}
w={20}
>
{t('common:common.Confirm')}
@@ -531,10 +575,7 @@ const InputTypeConfig = ({
{!isEdit && (
<Button
fontWeight={'medium'}
onClick={handleSubmit(
(data: FlowNodeInputItemType) => onSubmitSuccess(data, 'continue'),
onSubmitError
)}
onClick={handleSubmit((data) => onSubmitSuccess(data, 'continue'), onSubmitError)}
w={20}
>
{t('common:common.Continue_Adding')}
@@ -545,4 +586,4 @@ const InputTypeConfig = ({
);
};
export default React.memo(InputTypeConfig);
export default InputTypeConfig;

View File

@@ -53,24 +53,28 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
[chatConfig, setAppDetail]
);
return (
<NodeCard
selected={selected}
menuForbid={{
debug: true,
copy: true,
delete: true
}}
{...data}
>
<Container w={'360px'}>
<Instruction {...componentsProps} />
<Box pt={4}>
<FileSelectConfig {...componentsProps} />
</Box>
</Container>
</NodeCard>
);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
menuForbid={{
debug: true,
copy: true,
delete: true
}}
{...data}
>
<Container w={'360px'}>
<Instruction {...componentsProps} />
<Box pt={4}>
<FileSelectConfig {...componentsProps} />
</Box>
</Container>
</NodeCard>
);
}, [componentsProps, data, selected]);
return Render;
};
export default React.memo(NodePluginConfig);
@@ -114,46 +118,57 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
}
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
const pluginInputNode = nodes.find((item) => item.type === FlowNodeTypeEnum.pluginInput)!;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const pluginInputNode = nodeList.find(
(item) => item.flowNodeType === FlowNodeTypeEnum.pluginInput
)!;
return (
<FileSelect
value={fileSelectConfig}
color={'myGray.600'}
fontWeight={'medium'}
fontSize={'14px'}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
fileSelectConfig: e
}
}));
<>
<FileSelect
value={fileSelectConfig}
color={'myGray.600'}
fontWeight={'medium'}
fontSize={'sm'}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
fileSelectConfig: e
}
}));
// Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = pluginInputNode?.data.outputs.find(
(item) => item.key === userFilesInput.key
);
if (canUploadFiles) {
!repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'addOutput',
value: userFilesInput
});
} else {
repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'delOutput',
key: userFilesInput.key
});
}
}}
/>
// Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = pluginInputNode?.outputs.find(
(item) => item.key === userFilesInput.key
);
if (canUploadFiles) {
!repeatKey &&
onChangeNode({
nodeId: pluginInputNode.nodeId,
type: 'addOutput',
value: {
...userFilesInput,
label: t('workflow:plugin.global_file_input')
}
});
} else {
repeatKey &&
onChangeNode({
nodeId: pluginInputNode.nodeId,
type: 'delOutput',
key: userFilesInput.key
});
}
}}
/>
<Box fontSize={'mini'} color={'myGray.500'}>
{t('workflow:plugin_file_abandon_tip')}
</Box>
</>
);
}

View File

@@ -141,7 +141,7 @@ const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
}}
/>
</Container>
{!!outputs.length && (
{outputs.length != inputs.length && (
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />

View File

@@ -2,18 +2,12 @@ import React, { useCallback, useMemo, useState } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import Container from '../../components/Container';
import { FlowNodeInputItemType, ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { VARIABLE_NODE_ID, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputItemType, ReferenceValueType } from '@fastgpt/global/core/workflow/type/io';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import RenderInput from '../render/RenderInput';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import IOTitle from '../../components/IOTitle';
@@ -24,7 +18,6 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useI18n } from '@/web/context/I18n';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import PluginOutputEditModal, { defaultOutput } from './PluginOutputEditModal';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
@@ -113,38 +106,28 @@ function Reference({
content: workflowT('confirm_delete_field_tip')
});
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const [editField, setEditField] = useState<FlowNodeInputItemType>();
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
const value =
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e;
(e?: ReferenceValueType) => {
if (!e) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
value
value: e
}
});
},
[input, nodeId, nodeList, onChangeNode]
[input, nodeId, onChangeNode]
);
const { referenceList, formatValue } = useReference({
const { referenceList } = useReference({
nodeId,
valueType: input.valueType,
value: input.value
valueType: input.valueType
});
const onUpdateField = useCallback(
@@ -217,8 +200,9 @@ function Reference({
<ReferSelector
placeholder={t((input.referencePlaceholder as any) || 'select_reference_variable')}
list={referenceList}
value={formatValue}
value={input.value}
onSelect={onSelect}
isArray={input.valueType?.includes('array')}
/>
{!!editField && (

View File

@@ -15,7 +15,7 @@ import { WorkflowContext } from '../../context';
const NodeSimple = ({
data,
selected,
minW = '350px',
minW = '524px',
maxW
}: NodeProps<FlowNodeItemType> & { minW?: string | number; maxW?: string | number }) => {
const { t } = useTranslation();

View File

@@ -21,6 +21,7 @@ import WelcomeTextConfig from '@/components/core/app/WelcomeTextConfig';
import FileSelect from '@/components/core/app/FileSelect';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { userFilesInput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import Container from '../components/Container';
type ComponentProps = {
chatConfig: AppChatConfigType;
@@ -28,7 +29,8 @@ type ComponentProps = {
};
const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
const chatConfig = useMemo<AppChatConfigType>(() => {
return getAppChatConfig({
@@ -46,45 +48,48 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
[chatConfig, setAppDetail]
);
return (
<>
<NodeCard
minW={'300px'}
selected={selected}
menuForbid={{
debug: true,
copy: true,
delete: true
}}
{...data}
>
<Box px={4} py={'10px'} position={'relative'} borderRadius={'md'} className="nodrag">
<WelcomeText {...componentsProps} />
<Box pt={4}>
<ChatStartVariable {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'}>
<FileSelectConfig {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'}>
<TTSGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'}>
<WhisperGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'}>
<QuestionGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'}>
<ScheduledTrigger {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'}>
<QuestionInputGuide {...componentsProps} />
</Box>
</Box>
</NodeCard>
</>
);
const Render = useMemo(() => {
return (
<>
<NodeCard
selected={selected}
menuForbid={{
debug: true,
copy: true,
delete: true
}}
{...data}
>
<Container>
<WelcomeText {...componentsProps} />
<Box mt={2} pt={2}>
<ChatStartVariable {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
<FileSelectConfig {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
<TTSGuide {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
<WhisperGuide {...componentsProps} />
</Box>
<Box mt={3} pt={4} borderTop={'base'} borderColor={'myGray.200'}>
<QuestionGuide {...componentsProps} />
</Box>
<Box mt={4} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
<ScheduledTrigger {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
<QuestionInputGuide {...componentsProps} />
</Box>
</Container>
</NodeCard>
</>
);
}, [componentsProps, data, selected]);
return Render;
};
export default React.memo(NodeUserGuide);
@@ -217,8 +222,10 @@ function QuestionInputGuide({ chatConfig: { chatInputGuide }, setAppDetail }: Co
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
const workflowStartNode = nodes.find((item) => item.type === FlowNodeTypeEnum.workflowStart)!;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const workflowStartNode = nodeList.find(
(item) => item.flowNodeType === FlowNodeTypeEnum.workflowStart
)!;
return (
<FileSelect
@@ -234,20 +241,20 @@ function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: Co
// Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = workflowStartNode?.data.outputs.find(
const repeatKey = workflowStartNode?.outputs.find(
(item) => item.key === userFilesInput.key
);
if (canUploadFiles) {
!repeatKey &&
onChangeNode({
nodeId: workflowStartNode.id,
nodeId: workflowStartNode.nodeId,
type: 'addOutput',
value: userFilesInput
});
} else {
repeatKey &&
onChangeNode({
nodeId: workflowStartNode.id,
nodeId: workflowStartNode.nodeId,
type: 'delOutput',
key: userFilesInput.key
});

View File

@@ -26,7 +26,7 @@ const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<Box position={'relative'}>
<Box borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
<Box mb={-4} borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
<Divider
showBorderBottom={false}
icon={<MyIcon name="phoneTabbar/tool" w={'16px'} h={'16px'} />}

View File

@@ -63,9 +63,8 @@ const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
{t('common:option') + (i + 1)}
</Box>
</HStack>
<Box position={'relative'}>
<Box position={'relative'} mt={1}>
<Input
mt={1}
defaultValue={item.value}
bg={'white'}
fontSize={'sm'}
@@ -94,7 +93,7 @@ const NodeUserSelect = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', item.key)}
position={Position.Right}
translate={[26, 0]}
translate={[34, 0]}
/>
</Box>
</Box>

View File

@@ -26,14 +26,15 @@ import Container from '../components/Container';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { SmallAddIcon } from '@chakra-ui/icons';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { ReferenceItemValueType, ReferenceValueType } from '@fastgpt/global/core/workflow/type/io';
import { ReferSelector, useReference } from './render/RenderInput/templates/Reference';
import { getRefData } from '@/web/core/workflow/utils';
import { isReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { AppContext } from '@/pages/app/detail/components/context';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { useCreation, useMemoizedFn } from 'ahooks';
import { getEditorVariables } from '../../utils';
import { isArray } from 'lodash';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data;
@@ -42,7 +43,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const menuList = useRef([
{
@@ -103,28 +104,27 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
(item) => item.renderType === updateItem.renderType
);
const nodeIds = nodeList.map((node) => node.nodeId);
const handleUpdate = (newValue: ReferenceValueProps | string) => {
if (isReferenceValue(newValue, nodeIds)) {
const handleUpdate = (newValue?: ReferenceValueType | string) => {
if (typeof newValue === 'string') {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: newValue as ReferenceValueProps } : update
i === index ? { ...update, value: ['', newValue] } : update
)
);
} else {
} else if (newValue) {
onUpdateList(
updateList.map((update, i) =>
i === index ? { ...update, value: ['', newValue as string] } : update
i === index ? { ...update, value: newValue as ReferenceItemValueType } : update
)
);
}
};
return (
<Container key={index} mt={4} w={'full'} mx={0}>
<Container key={index} w={'full'} mx={0}>
<Flex alignItems={'center'}>
<Flex w={'60px'}>{t('common:core.workflow.variable')}</Flex>
<Reference
<VariableSelector
nodeId={nodeId}
variable={updateItem.variable}
onSelect={(value) => {
@@ -135,7 +135,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
...update,
value: ['', ''],
valueType,
variable: value
variable: value as ReferenceItemValueType
};
}
return update;
@@ -181,7 +181,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
if (i === index) {
return {
...update,
value: ['', ''],
value: undefined,
renderType:
updateItem.renderType === FlowNodeInputTypeEnum.input
? FlowNodeInputTypeEnum.reference
@@ -202,7 +202,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
{(() => {
if (updateItem.renderType === FlowNodeInputTypeEnum.reference) {
return (
<Reference
<VariableSelector
nodeId={nodeId}
variable={updateItem.value}
valueType={valueType}
@@ -210,11 +210,14 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
/>
);
}
const inputValue = isArray(updateItem.value?.[1]) ? '' : updateItem.value?.[1];
if (valueType === WorkflowIOValueTypeEnum.string) {
return (
<Box w={'300px'}>
<PromptEditor
value={updateItem.value?.[1] || ''}
value={inputValue || ''}
onChange={handleUpdate}
showOpenModal={false}
variableLabels={variables}
@@ -225,7 +228,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
}
if (valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput value={Number(updateItem.value?.[1]) || 0}>
<NumberInput value={Number(inputValue) || 0}>
<NumberInputField bg="white" onChange={(e) => handleUpdate(e.target.value)} />
<NumberInputStepper>
<NumberIncrementStepper />
@@ -237,7 +240,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
defaultChecked={updateItem.value?.[1] === 'true'}
defaultChecked={inputValue === 'true'}
onChange={(e) => handleUpdate(String(e.target.checked))}
/>
);
@@ -246,7 +249,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
return (
<Box w={'300px'}>
<PromptEditor
value={updateItem.value?.[1] || ''}
value={inputValue || ''}
onChange={handleUpdate}
showOpenModal={false}
variableLabels={variables}
@@ -261,66 +264,76 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
}
);
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} pb={4}>
<>
{updateList.map((updateItem, index) => (
<ValueRender key={index} updateItem={updateItem} index={index} />
))}
</>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
w={'full'}
size={'sm'}
onClick={() => {
onUpdateList([
...updateList,
{
variable: ['', ''],
value: ['', ''],
renderType: FlowNodeInputTypeEnum.input
}
]);
}}
const Render = useMemo(() => {
return (
<NodeCard selected={selected} maxW={'1000px'} {...data}>
<Box px={4} pb={4}>
<Flex flexDirection={'column'} gap={4}>
{updateList.map((updateItem, index) => (
<ValueRender key={index} updateItem={updateItem} index={index} />
))}
</Flex>
<Flex
className="nodrag"
cursor={'default'}
alignItems={'center'}
position={'relative'}
mt={4}
>
{t('common:common.Add New')}
</Button>
</Flex>
</Box>
</NodeCard>
);
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
w={'full'}
size={'sm'}
onClick={() => {
onUpdateList([
...updateList,
{
variable: ['', ''],
value: ['', ''],
renderType: FlowNodeInputTypeEnum.input
}
]);
}}
>
{t('common:common.Add New')}
</Button>
</Flex>
</Box>
</NodeCard>
);
}, [ValueRender, data, onUpdateList, selected, t, updateList]);
return Render;
};
export default React.memo(NodeVariableUpdate);
const Reference = ({
const VariableSelector = ({
nodeId,
variable,
valueType,
onSelect
}: {
nodeId: string;
variable?: ReferenceValueProps;
variable?: ReferenceValueType;
valueType?: WorkflowIOValueTypeEnum;
onSelect: (e: ReferenceValueProps) => void;
onSelect: (e?: ReferenceValueType) => void;
}) => {
const { t } = useTranslation();
const { referenceList, formatValue } = useReference({
const { referenceList } = useReference({
nodeId,
valueType,
value: variable
valueType
});
return (
<ReferSelector
placeholder={t('common:select_reference_variable')}
list={referenceList}
value={formatValue}
value={variable}
onSelect={onSelect}
isArray={valueType?.includes('array')}
/>
);
};

View File

@@ -24,8 +24,8 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider';
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, outputs } = data;
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const customGlobalVariables = useCreation(() => {
const globalVariables = formatEditorVariablePickerIcon(
@@ -62,34 +62,36 @@ const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
})),
[t]
);
const Render = useMemo(() => {
return (
<NodeCard
selected={selected}
menuForbid={{
copy: true,
delete: true
}}
{...data}
>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<Container>
<IOTitle text={t('common:core.module.Variable')} />
{customGlobalVariables.length > 0 && (
<>
<RenderOutput nodeId={nodeId} flowOutputList={customGlobalVariables} />
<MyDivider />
</>
)}
return (
<NodeCard
minW={'240px'}
selected={selected}
menuForbid={{
copy: true,
delete: true
}}
{...data}
>
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<Container>
<IOTitle text={t('common:core.module.Variable')} />
{customGlobalVariables.length > 0 && (
<>
<RenderOutput nodeId={nodeId} flowOutputList={customGlobalVariables} />
<MyDivider />
</>
)}
<RenderOutput nodeId={nodeId} flowOutputList={systemVariables} />
</Container>
</NodeCard>
);
}, [customGlobalVariables, data, nodeId, outputs, selected, systemVariables, t]);
<RenderOutput nodeId={nodeId} flowOutputList={systemVariables} />
</Container>
</NodeCard>
);
return Render;
};
export default React.memo(NodeStart);

View File

@@ -5,6 +5,7 @@ import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context';
import { WorkflowNodeEdgeContext } from '../../../../context/workflowInitContext';
export const ConnectionSourceHandle = ({
nodeId,
@@ -13,7 +14,8 @@ export const ConnectionSourceHandle = ({
nodeId: string;
isFoldNode?: boolean;
}) => {
const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
const { showSourceHandle, RightHandle, LeftHandlee, TopHandlee, BottomHandlee } = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId);
@@ -50,7 +52,7 @@ export const ConnectionSourceHandle = ({
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[2, 0]}
translate={[4, 0]}
/>
);
})();
@@ -67,7 +69,7 @@ export const ConnectionSourceHandle = ({
nodeId={nodeId}
handleId={handleId}
position={Position.Left}
translate={[-6, 0]}
translate={[-8, 0]}
/>
);
})();
@@ -90,7 +92,7 @@ export const ConnectionSourceHandle = ({
nodeId={nodeId}
handleId={handleId}
position={Position.Top}
translate={[0, -2]}
translate={[0, -5]}
/>
);
})();
@@ -106,7 +108,7 @@ export const ConnectionSourceHandle = ({
nodeId={nodeId}
handleId={handleId}
position={Position.Bottom}
translate={[0, 2]}
translate={[0, 5]}
/>
);
})();
@@ -135,7 +137,8 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
}: {
nodeId: string;
}) {
const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const { connectingEdge, nodeList } = useContextSelector(WorkflowContext, (ctx) => ctx);
const { LeftHandle, rightHandle, topHandle, bottomHandle } = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId);
@@ -191,7 +194,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Left}
translate={[-2, 0]}
translate={[-4, 0]}
showHandle={showHandle}
/>
);
@@ -206,7 +209,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Right}
translate={[2, 0]}
translate={[4, 0]}
showHandle={showHandle}
/>
);
@@ -221,7 +224,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Top}
translate={[0, -2]}
translate={[0, -5]}
showHandle={showHandle}
/>
);
@@ -236,7 +239,7 @@ export const ConnectionTargetHandle = React.memo(function ConnectionTargetHandle
nodeId={nodeId}
handleId={handleId}
position={Position.Bottom}
translate={[0, 2]}
translate={[0, 5]}
showHandle={showHandle}
/>
);

View File

@@ -6,6 +6,7 @@ import { Connection, Handle, Position } from 'reactflow';
import { useCallback, useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { WorkflowNodeEdgeContext } from '../../../../context/workflowInitContext';
const handleSize = '16px';
@@ -16,7 +17,7 @@ type ToolHandleProps = BoxProps & {
export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
const { t } = useTranslation();
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const handleId = NodeOutputKeyEnum.selectedTools;
@@ -64,7 +65,7 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
export const ToolSourceHandle = () => {
const { t } = useTranslation();
const setEdges = useContextSelector(WorkflowContext, (v) => v.setEdges);
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
/* onConnect edge, delete tool input and switch */
const onConnect = useCallback(

View File

@@ -1,10 +1,15 @@
import React, { useMemo } from 'react';
import { Handle, Position } from 'reactflow';
import { SmallAddIcon } from '@chakra-ui/icons';
import { handleHighLightStyle, sourceCommonStyle, handleConnectedStyle, handleSize } from './style';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../../context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
WorkflowNodeEdgeContext,
WorkflowInitContext
} from '../../../../context/workflowInitContext';
import { WorkflowEventContext } from '../../../../context/workflowEventContext';
type Props = {
nodeId: string;
@@ -24,11 +29,10 @@ const MySourceHandle = React.memo(function MySourceHandle({
highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>;
}) {
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
const hoverNodeId = useContextSelector(WorkflowContext, (v) => v.hoverNodeId);
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const hoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.hoverNodeId);
const node = useMemo(() => nodes.find((node) => node.data.nodeId === nodeId), [nodes, nodeId]);
const connected = edges.some((edge) => edge.sourceHandle === handleId);
@@ -109,7 +113,13 @@ const MySourceHandle = React.memo(function MySourceHandle({
isConnectableEnd={false}
>
{showAddIcon && (
<SmallAddIcon pointerEvents={'none'} color={'primary.600'} fontWeight={'bold'} />
<MyIcon
name={'edgeAdd'}
color={'primary.500'}
pointerEvents={'none'}
w={'14px'}
h={'14px'}
/>
)}
</Handle>
);
@@ -144,7 +154,8 @@ const MyTargetHandle = React.memo(function MyTargetHandle({
highlightStyle: Record<string, any>;
connectedStyle: Record<string, any>;
}) {
const { connectingEdge, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
const connected = edges.some((edge) => edge.targetHandle === handleId);

View File

@@ -1,8 +1,8 @@
export const primaryColor = '#3370FF';
export const primaryColor = '#487FFF';
export const lowPrimaryColor = '#94B5FF';
export const handleSize = {
width: '18px',
height: '18px'
width: '20px',
height: '20px'
};
export const sourceCommonStyle = {
@@ -12,16 +12,16 @@ export const sourceCommonStyle = {
};
export const handleConnectedStyle = {
borderColor: lowPrimaryColor,
width: '14px',
height: '14px'
width: '16px',
height: '16px'
};
export const handleHighLightStyle = {
borderColor: primaryColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '18px',
height: '18px'
width: '20px',
height: '20px'
};
export default function Dom() {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Button, Card, Flex, FlexProps, Image } from '@chakra-ui/react';
import { Box, Button, Card, Flex, FlexProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
@@ -15,7 +15,7 @@ import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/Connect
import { useDebug } from '../../hooks/useDebug';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { getPreviewPluginNode } from '@/web/core/app/api/plugin';
import { storeNode2FlowNode, getLatestNodeTemplate } from '@/web/core/workflow/utils';
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
@@ -25,8 +25,10 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useWorkflowUtils } from '../../hooks/useUtils';
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getDocPath } from '@/web/common/system/doc';
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
import { WorkflowEventContext } from '../../../context/workflowEventContext';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
@@ -69,10 +71,10 @@ const NodeCard = (props: Props) => {
customStyle
} = props;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const setHoverNodeId = useContextSelector(WorkflowContext, (v) => v.setHoverNodeId);
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
const setHoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverNodeId);
// custom title edit
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
@@ -149,15 +151,17 @@ const NodeCard = (props: Props) => {
<Box position={'relative'}>
{/* debug */}
{showHeader && (
<Box px={4} py={3}>
<Box px={3} pt={4}>
{/* tool target handle */}
<ToolTargetHandle show={showToolHandle} nodeId={nodeId} />
{/* avatar and name */}
<Flex alignItems={'center'}>
{node?.flowNodeType !== FlowNodeTypeEnum.stopTool && (
<Box
mr={2}
<Flex
alignItems={'center'}
mr={1}
p={1}
cursor={'pointer'}
rounded={'sm'}
_hover={{ bg: 'myGray.200' }}
@@ -172,20 +176,20 @@ const NodeCard = (props: Props) => {
>
<MyIcon
name={!isFolded ? 'core/chat/chevronDown' : 'core/chat/chevronRight'}
w={'24px'}
h={'24px'}
w={'16px'}
h={'16px'}
color={'myGray.500'}
/>
</Box>
</Flex>
)}
<Avatar
src={avatar}
borderRadius={'sm'}
objectFit={'contain'}
w={'30px'}
h={'30px'}
w={'24px'}
h={'24px'}
/>
<Box ml={3} fontSize={'md'} fontWeight={'medium'}>
<Box ml={2} fontSize={'18px'} fontWeight={'medium'} color={'myGray.900'}>
{t(name as any)}
</Box>
<MyIcon
@@ -241,7 +245,7 @@ const NodeCard = (props: Props) => {
{!!nodeTemplate?.diagram && !hasNewVersion && (
<MyTooltip
label={
<Image
<MyImage
src={nodeTemplate?.diagram}
w={'100%'}
minH={['auto', '200px']}
@@ -280,7 +284,7 @@ const NodeCard = (props: Props) => {
</MyTooltip>
)}
</Flex>
<NodeIntro nodeId={nodeId} intro={intro} />
{intro && <NodeIntro nodeId={nodeId} intro={intro} />}
</Box>
)}
<MenuRender nodeId={nodeId} menuForbid={menuForbid} nodeList={nodeList} />
@@ -330,13 +334,16 @@ const NodeCard = (props: Props) => {
maxW={maxW}
minH={minH}
bg={'white'}
borderWidth={'1px'}
borderRadius={'md'}
boxShadow={'1'}
outline={selected ? '2px solid' : '1px solid'}
borderRadius={'lg'}
boxShadow={
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
}
w={w}
h={h}
_hover={{
boxShadow: '4',
boxShadow:
'0px 12px 16px -4px rgba(19, 51, 107, 0.20), 0px 0px 1px 0px rgba(19, 51, 107, 0.20)',
'& .controller-menu': {
display: 'flex'
},
@@ -351,17 +358,19 @@ const NodeCard = (props: Props) => {
onMouseLeave={() => setHoverNodeId(undefined)}
{...(isError
? {
borderColor: 'red.500',
outlineColor: 'red.500',
onMouseDownCapture: () => onUpdateNodeError(nodeId, false)
}
: {
borderColor: selected ? 'primary.600' : 'borderColor.base'
outlineColor: selected ? 'primary.600' : 'myGray.250'
})}
{...customStyle}
>
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
{Header}
{!isFolded && children}
<Flex flexDirection={'column'} flex={1} my={!isFolded ? 4 : 0} gap={2}>
{!isFolded ? children : <Box h={4} />}
</Flex>
{RenderHandle}
{RenderToolHandle}
@@ -385,7 +394,8 @@ const MenuRender = React.memo(function MenuRender({
const { t } = useTranslation();
const { openDebugNode, DebugInputModal } = useDebug();
const { setNodes, setEdges, onNodesChange } = useContextSelector(WorkflowContext, (v) => v);
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
const { computedNewNodeName } = useWorkflowUtils();
const onCopyNode = useCallback(
@@ -568,31 +578,35 @@ const NodeIntro = React.memo(function NodeIntro({
const Render = useMemo(() => {
return (
<>
<Flex alignItems={'flex-end'} py={1}>
<Box fontSize={'xs'} color={'myGray.600'} flex={'1 0 0'}>
<Flex alignItems={'center'}>
<Box fontSize={'sm'} color={'myGray.500'} flex={'1 0 0'}>
{t(intro as any)}
</Box>
{NodeIsTool && (
<Button
size={'xs'}
variant={'whiteBase'}
onClick={() => {
onOpenIntroModal({
defaultVal: intro,
onSuccess(e) {
onChangeNode({
nodeId,
type: 'attr',
key: 'intro',
value: e
});
}
});
}}
>
{t('common:core.module.Edit intro')}
</Button>
)}
<Flex
p={'7px'}
rounded={'sm'}
alignItems={'center'}
_hover={{
bg: NodeIsTool ? 'myGray.100' : 'transparent'
}}
cursor={NodeIsTool ? 'pointer' : 'default'}
onClick={() => {
if (!NodeIsTool) return;
onOpenIntroModal({
defaultVal: intro,
onSuccess(e) {
onChangeNode({
nodeId,
type: 'attr',
key: 'intro',
value: e
});
}
});
}}
>
<MyIcon name={'edit'} w={'18px'} opacity={NodeIsTool ? 1 : 0} />
</Flex>
</Flex>
<EditIntroModal maxLength={500} />
</>
@@ -654,7 +668,7 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
return !!debugResult && !!statusData ? (
<>
<Flex px={4} bg={statusData.bg} borderTopRadius={'md'} py={3}>
<Flex px={3} bg={statusData.bg} borderTopRadius={'md'} py={3}>
<MyIcon name={statusData.icon as any} w={'16px'} mr={2} />
<Box color={'myGray.900'} fontWeight={'bold'} flex={'1 0 0'}>
{statusData.text}
@@ -695,7 +709,7 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
border={'base'}
>
{/* Status header */}
<Flex h={'54x'} px={4} py={3} alignItems={'center'}>
<Flex h={'54x'} px={3} py={3} alignItems={'center'}>
<MyIcon mr={1} name={'core/workflow/debugResult'} w={'20px'} color={'primary.600'} />
<Box fontWeight={'bold'} flex={'1'}>
{t('common:core.workflow.debug.Run result')}
@@ -741,7 +755,7 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
{debugResult.message}
</Box>
)}
{response && <WholeResponseContent activeModule={response} showDetail />}
{response && <WholeResponseContent activeModule={response} />}
</Box>
)}
</Card>

View File

@@ -42,58 +42,41 @@ const InputLabel = ({ nodeId, input }: Props) => {
},
[input, nodeId, onChangeNode, renderTypeList]
);
const renderType = renderTypeList?.[selectedTypeIndex || 0];
const RenderLabel = useMemo(() => {
const renderType = renderTypeList?.[selectedTypeIndex || 0];
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<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 */}
{renderType === FlowNodeInputTypeEnum.reference && (
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
)}
{/* input type select */}
{renderTypeList && renderTypeList.length > 1 && (
<Box ml={2}>
<NodeInputSelect
renderTypeList={renderTypeList}
renderTypeIndex={selectedTypeIndex}
onChange={onChangeRenderType}
/>
</Box>
)}
{/* Variable picker tip */}
{input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && (
<>
<Box flex={1} />
<VariableTip transform={'translateY(2px)'} />
</>
)}
return (
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<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>
);
}, [
description,
input.renderTypeList,
input.selectedTypeIndex,
label,
onChangeRenderType,
renderTypeList,
required,
selectedTypeIndex,
t,
valueDesc,
valueType
]);
{/* value type */}
{[FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.fileSelect].includes(renderType) && (
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
)}
return RenderLabel;
{/* input type select */}
{renderTypeList && renderTypeList.length > 1 && (
<Box ml={2}>
<NodeInputSelect
renderTypeList={renderTypeList}
renderTypeIndex={selectedTypeIndex}
onChange={onChangeRenderType}
/>
</Box>
)}
{/* Variable picker tip */}
{input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && (
<>
<Box flex={1} />
<VariableTip transform={'translateY(2px)'} />
</>
)}
</Flex>
);
};
export default React.memo(InputLabel);

View File

@@ -16,6 +16,10 @@ const RenderList: {
types: [FlowNodeInputTypeEnum.reference],
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.fileSelect],
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.select],
Component: dynamic(() => import('./templates/Select'))
@@ -81,54 +85,45 @@ type Props = {
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
const { feConfigs } = useSystemStore();
const copyInputs = useMemo(
() =>
JSON.stringify(
flowInputList.filter((input) => {
if (input.isPro && !feConfigs?.isPlus) return false;
return true;
})
),
[feConfigs?.isPlus, flowInputList]
);
const filterInputs = useMemo(() => {
return JSON.parse(copyInputs) as FlowNodeInputItemType[];
}, [copyInputs]);
const memoCustomComponent = useMemo(() => CustomComponent || {}, [CustomComponent]);
const Render = useMemo(() => {
return filterInputs.map((input) => {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
const isDynamic = !!input.canEdit;
const RenderComponent = (() => {
if (renderType === FlowNodeInputTypeEnum.custom && memoCustomComponent[input.key]) {
return <>{memoCustomComponent[input.key]({ ...input })}</>;
}
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
if (!Component) return null;
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
})();
return renderType !== FlowNodeInputTypeEnum.hidden && !isDynamic ? (
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
{!!input.label && !hideLabelTypeList.includes(renderType) && (
<InputLabel nodeId={nodeId} input={input} />
)}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
</Box>
)}
</Box>
) : null;
return flowInputList.filter((input) => {
if (input.isPro && !feConfigs?.isPlus) return false;
return true;
});
}, [filterInputs, mb, memoCustomComponent, nodeId]);
}, [feConfigs?.isPlus, flowInputList]);
return <>{Render}</>;
return (
<>
{filterInputs.map((input) => {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
const isDynamic = !!input.canEdit;
const RenderComponent = (() => {
if (renderType === FlowNodeInputTypeEnum.custom && CustomComponent?.[input.key]) {
return <>{CustomComponent?.[input.key]({ ...input })}</>;
}
const Component = RenderList.find((item) => item.types.includes(renderType))?.Component;
if (!Component) return null;
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
})();
return renderType !== FlowNodeInputTypeEnum.hidden && !isDynamic ? (
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
{!!input.label && !hideLabelTypeList.includes(renderType) && (
<InputLabel nodeId={nodeId} input={input} />
)}
{!!RenderComponent && (
<Box mt={2} className={'nodrag'}>
{RenderComponent}
</Box>
)}
</Box>
) : null;
})}
</>
);
};
export default React.memo(RenderInput);

View File

@@ -5,20 +5,17 @@ import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType, ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import { FlowNodeInputItemType, ReferenceValueType } from '@fastgpt/global/core/workflow/type/io';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { defaultInput } from '../../FieldEditModal';
import { getInputComponentProps } from '@fastgpt/global/core/workflow/node/io/utils';
import { VARIABLE_NODE_ID } from '@fastgpt/global/core/workflow/constants';
import { ReferSelector, useReference } from '../Reference';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import ValueTypeLabel from '../../../ValueTypeLabel';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
const FieldEditModal = dynamic(() => import('../../FieldEditModal'));
@@ -126,15 +123,37 @@ function Reference({
const [editField, setEditField] = useState<FlowNodeInputItemType>();
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
(e?: ReferenceValueType) => {
if (!e) return;
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: {
...inputChildren,
value: e
}
});
},
[inputChildren, nodeId, onChangeNode]
);
const newValue =
e[0] === workflowStartNode?.id && !isWorkflowStartOutput(e[1])
? [VARIABLE_NODE_ID, e[1]]
: e;
const { referenceList } = useReference({
nodeId,
valueType: inputChildren.valueType
});
const onUpdateField = useCallback(
({ data }: { data: FlowNodeInputItemType }) => {
if (!data.key) return;
const oldType = inputChildren.valueType;
const newType = data.valueType;
let newValue = data.value;
if (oldType?.includes('array') && !newType?.includes('array')) {
newValue = data.value[0];
} else if (!oldType?.includes('array') && newType?.includes('array')) {
newValue = [data.value];
}
onChangeNode({
nodeId,
@@ -142,31 +161,14 @@ function Reference({
key: inputChildren.key,
value: {
...inputChildren,
value: newValue
value: newValue,
key: data.key,
label: data.label,
valueType: data.valueType
}
});
},
[inputChildren, nodeId, nodeList, onChangeNode]
);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: inputChildren.valueType,
value: inputChildren.value
});
const onUpdateField = useCallback(
({ data }: { data: FlowNodeInputItemType }) => {
if (!data.key) return;
onChangeNode({
nodeId,
type: 'replaceInput',
key: inputChildren.key,
value: data
});
},
[inputChildren.key, nodeId, onChangeNode]
[inputChildren, nodeId, onChangeNode]
);
const onDel = useCallback(() => {
onChangeNode({
@@ -210,8 +212,9 @@ function Reference({
<ReferSelector
placeholder={t((inputChildren.referencePlaceholder as any) || 'select_reference_variable')}
list={referenceList}
value={formatValue}
value={inputChildren.value}
onSelect={onSelect}
isArray={inputChildren.valueType?.includes('array')}
/>
{!!editField && !!item.customInputConfig && (

View File

@@ -51,24 +51,20 @@ const JsonEditor = ({ inputs = [], item, nodeId }: RenderInputProps) => {
return JSON.stringify(item.value, null, 2);
}, [item.value]);
const Render = useMemo(() => {
return (
<JSONEditor
className="nowheel"
bg={'white'}
borderRadius={'sm'}
placeholder={t(item.placeholder as any)}
resize
value={value}
onChange={(e) => {
update(e);
}}
variables={variables}
/>
);
}, [item.placeholder, t, update, value, variables]);
return Render;
return (
<JSONEditor
className="nowheel"
bg={'white'}
borderRadius={'sm'}
placeholder={t(item.placeholder as any)}
resize
value={value}
onChange={(e) => {
update(e);
}}
variables={variables}
/>
);
};
export default React.memo(JsonEditor);

View File

@@ -9,6 +9,7 @@ import {
} from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import MyIcon from '@fastgpt/web/components/common/Icon';
const NumberInputRender = ({ item, nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
@@ -19,6 +20,8 @@ const NumberInputRender = ({ item, nodeId }: RenderInputProps) => {
defaultValue={item.value}
min={item.min}
max={item.max}
bg={'white'}
rounded={'md'}
onChange={(e) => {
onChangeNode({
nodeId,
@@ -31,10 +34,31 @@ const NumberInputRender = ({ item, nodeId }: RenderInputProps) => {
});
}}
>
<NumberInputField bg={'white'} px={3} borderRadius={'sm'} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
<NumberInputField
bg={'white'}
px={3}
rounded={'md'}
_hover={{
borderColor: 'primary.500'
}}
/>
<NumberInputStepper roundedTopRight={'none'}>
<NumberIncrementStepper
borderTopRightRadius={'sm !important'}
_hover={{
bg: 'myGray.100'
}}
>
<MyIcon name={'core/chat/chevronUp'} width={'12px'} />
</NumberIncrementStepper>
<NumberDecrementStepper
borderBottomRightRadius={'sm !important'}
_hover={{
bg: 'myGray.100'
}}
>
<MyIcon name={'core/chat/chevronDown'} width={'12px'} />
</NumberDecrementStepper>
</NumberInputStepper>
</NumberInput>
);

View File

@@ -1,28 +1,41 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Flex, Box, ButtonProps } from '@chakra-ui/react';
import { Flex, Box, ButtonProps, Grid } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { computedNodeInputReference } from '@/web/core/workflow/utils';
import {
computedNodeInputReference,
filterWorkflowNodeOutputsByType
} from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import {
NodeOutputKeyEnum,
VARIABLE_NODE_ID,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import type { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io';
import type {
ReferenceArrayValueType,
ReferenceItemValueType,
ReferenceValueType
} from '@fastgpt/global/core/workflow/type/io';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '@/pages/app/detail/components/context';
import { WorkflowNodeEdgeContext } from '../../../../../context/workflowInitContext';
const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
const MultipleRowSelect = dynamic(() =>
import('@fastgpt/web/components/common/MySelect/MultipleRowSelect').then(
(v) => v.MultipleRowSelect
)
);
const MultipleRowArraySelect = dynamic(() =>
import('@fastgpt/web/components/common/MySelect/MultipleRowSelect').then(
(v) => v.MultipleRowArraySelect
)
);
const Avatar = dynamic(() => import('@fastgpt/web/components/common/Avatar'));
type SelectProps = {
value?: ReferenceValueProps;
type CommonSelectProps = {
placeholder?: string;
list: {
label: string | React.ReactNode;
@@ -33,85 +46,28 @@ type SelectProps = {
valueType?: WorkflowIOValueTypeEnum;
}[];
}[];
onSelect: (val: ReferenceValueProps) => void;
popDirection?: 'top' | 'bottom';
styles?: ButtonProps;
};
const Reference = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onSelect = useCallback(
(e: ReferenceValueProps) => {
const workflowStartNode = nodeList.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.workflowStart
);
if (e[0] === workflowStartNode?.id && e[1] !== NodeOutputKeyEnum.userChatInput) {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: [VARIABLE_NODE_ID, e[1]]
}
});
} else {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
}
},
[item, nodeId, nodeList, onChangeNode]
);
const { referenceList, formatValue } = useReference({
nodeId,
valueType: item.valueType,
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}
/>
);
type SelectProps<T extends boolean> = CommonSelectProps & {
isArray?: T;
value?: T extends true ? ReferenceArrayValueType : ReferenceItemValueType;
onSelect: (val?: T extends true ? ReferenceArrayValueType : ReferenceItemValueType) => void;
};
export default React.memo(Reference);
export const useReference = ({
nodeId,
valueType = WorkflowIOValueTypeEnum.any,
value
valueType = WorkflowIOValueTypeEnum.any
}: {
nodeId: string;
valueType?: WorkflowIOValueTypeEnum;
value?: any;
}) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
// 获取可选的变量列表
const referenceList = useMemo(() => {
const sourceNodes = computedNodeInputReference({
nodeId,
@@ -123,31 +79,24 @@ export const useReference = ({
if (!sourceNodes) return [];
const isArray = valueType?.includes('array');
// 转换为 select 的数据结构
const list: SelectProps['list'] = sourceNodes
const list: CommonSelectProps['list'] = sourceNodes
.map((node) => {
return {
label: (
<Flex alignItems={'center'}>
<Avatar src={node.avatar} w={'1.25rem'} borderRadius={'xs'} />
<Avatar src={node.avatar} w={isArray ? '1rem' : '1.25rem'} borderRadius={'xs'} />
<Box ml={1}>{t(node.name as any)}</Box>
</Flex>
),
value: node.nodeId,
children: node.outputs
.filter(
(output) =>
valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === WorkflowIOValueTypeEnum.any ||
output.valueType === valueType ||
// When valueType is arrayAny, return all array type outputs
(valueType === WorkflowIOValueTypeEnum.arrayAny &&
output.valueType?.includes('array'))
)
children: filterWorkflowNodeOutputsByType(node.outputs, valueType)
.filter((output) => output.id !== NodeOutputKeyEnum.addOutputParam)
.map((output) => {
return {
label: t((output.label as any) || ''),
label: t(output.label as any),
value: output.id,
valueType: output.valueType
};
@@ -159,68 +108,231 @@ export const useReference = ({
return list;
}, [appDetail.chatConfig, edges, nodeId, nodeList, t, valueType]);
const formatValue = useMemo(() => {
if (
Array.isArray(value) &&
value.length === 2 &&
typeof value[0] === 'string' &&
typeof value[1] === 'string'
) {
return value as ReferenceValueProps;
}
return undefined;
}, [value]);
return {
referenceList,
formatValue
referenceList
};
};
export const ReferSelector = ({
const Reference = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const isArray = item.valueType?.includes('array') ?? false;
const onSelect = useCallback(
(e?: ReferenceValueType) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: e
}
});
},
[item, nodeId, onChangeNode]
);
const { referenceList } = useReference({
nodeId,
valueType: item.valueType
});
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) || t('common:select_reference_variable')}
list={referenceList}
value={item.value}
onSelect={onSelect}
popDirection={popDirection}
isArray={isArray}
/>
);
};
export default React.memo(Reference);
const SingleReferenceSelector = ({
placeholder,
value,
list = [],
onSelect,
popDirection
}: SelectProps) => {
const selectItemLabel = useMemo(() => {
if (!value) {
return;
}
const firstColumn = list.find((item) => item.value === value[0]);
if (!firstColumn) {
return;
}
const secondColumn = firstColumn.children.find((item) => item.value === value[1]);
if (!secondColumn) {
return;
}
return [firstColumn, secondColumn];
}, [list, value]);
}: SelectProps<false>) => {
const getSelectValue = useCallback(
(value: ReferenceValueType) => {
if (!value) return [];
const firstColumn = list.find((item) => item.value === value[0]);
if (!firstColumn) {
return [];
}
const secondColumn = firstColumn.children.find((item) => item.value === value[1]);
if (!secondColumn) {
return [];
}
return [firstColumn.label, secondColumn.label];
},
[list]
);
const ItemSelector = useMemo(() => {
const selectorVal = value as ReferenceItemValueType;
const [nodeName, outputName] = getSelectValue(selectorVal);
const isValidSelect = nodeName && outputName;
const Render = useMemo(() => {
return (
<MultipleRowSelect
label={
selectItemLabel ? (
<Flex alignItems={'center'}>
{selectItemLabel[0].label}
<MyIcon name={'common/rightArrowLight'} mx={1} w={'14px'}></MyIcon>
{selectItemLabel[1].label}
isValidSelect ? (
<Flex gap={2} alignItems={'center'} fontSize={'sm'}>
<Flex py={1} pl={1} alignItems={'center'}>
{nodeName}
<MyIcon name={'common/rightArrowLight'} mx={1} w={'12px'} color={'myGray.500'} />
{outputName}
</Flex>
</Flex>
) : (
<Box>{placeholder}</Box>
<Box fontSize={'sm'} color={'myGray.400'}>
{placeholder}
</Box>
)
}
value={value as any[]}
value={selectorVal}
list={list}
onSelect={(e) => {
onSelect(e as ReferenceValueProps);
}}
onSelect={onSelect as any}
popDirection={popDirection}
/>
);
}, [list, onSelect, placeholder, popDirection, selectItemLabel, value]);
}, [getSelectValue, list, onSelect, placeholder, popDirection, value]);
return Render;
return ItemSelector;
};
const MultipleReferenceSelector = ({
placeholder,
value,
list = [],
onSelect,
popDirection
}: SelectProps<true>) => {
const { t } = useTranslation();
const getSelectValue = useCallback(
(value: ReferenceValueType) => {
if (!value) return [];
const firstColumn = list.find((item) => item.value === value[0]);
if (!firstColumn) {
return [];
}
const secondColumn = firstColumn.children.find((item) => item.value === value[1]);
if (!secondColumn) {
return [];
}
return [firstColumn.label, secondColumn.label];
},
[list]
);
// Get valid item and remove invalid item
const formatList = useMemo(() => {
if (!value) return [];
return value?.map((item) => {
const [nodeName, outputName] = getSelectValue(item);
return {
rawValue: item,
nodeName,
outputName
};
});
}, [getSelectValue, value]);
useEffect(() => {
const validList = formatList.filter((item) => item.nodeName && item.outputName);
if (validList.length !== value?.length) {
onSelect(validList.map((item) => item.rawValue));
}
}, [formatList, onSelect, value]);
const ArraySelector = useMemo(() => {
return (
<MultipleRowArraySelect
label={
formatList.length > 0 ? (
<Grid py={3} gridTemplateColumns={'1fr 1fr'} gap={2} fontSize={'sm'}>
{formatList.map(({ nodeName, outputName }, index) => {
if (!nodeName || !outputName) return null;
return (
<Flex
alignItems={'center'}
key={index}
bg={'primary.50'}
color={'myGray.900'}
py={1}
px={1.5}
rounded={'sm'}
>
<Flex
alignItems={'center'}
flex={'1 0 0'}
maxW={'200px'}
className="textEllipsis"
>
{nodeName}
<MyIcon
name={'common/rightArrowLight'}
mx={1}
w={'12px'}
color={'myGray.500'}
/>
{outputName}
</Flex>
<MyIcon
name={'common/closeLight'}
w={'1rem'}
ml={1}
cursor={'pointer'}
color={'myGray.500'}
_hover={{
color: 'red.600'
}}
onClick={(e) => {
e.stopPropagation();
onSelect(value?.filter((_, i) => i !== index));
}}
/>
</Flex>
);
})}
</Grid>
) : (
<Box fontSize={'sm'} color={'myGray.400'}>
{placeholder}
</Box>
)
}
value={value as any}
list={list}
onSelect={onSelect as any}
popDirection={popDirection}
/>
);
}, [formatList, list, onSelect, placeholder, popDirection, value]);
return ArraySelector;
};
export const ReferSelector = <T extends boolean>(props: SelectProps<T>) => {
return props.isArray ? (
<MultipleReferenceSelector {...(props as SelectProps<true>)} />
) : (
<SingleReferenceSelector {...(props as SelectProps<false>)} />
);
};

View File

@@ -302,9 +302,9 @@ const SettingQuotePrompt = (props: RenderInputProps) => {
return (
<>
<Flex className="nodrag" cursor={'default'} alignItems={'center'} position={'relative'}>
<Box position={'relative'} color={'myGray.600'} fontWeight={'medium'}>
<FormLabel position={'relative'} color={'myGray.600'} fontWeight={'medium'}>
{t('common:core.module.Dataset quote.label')}
</Box>
</FormLabel>
<ValueTypeLabel
valueType={WorkflowIOValueTypeEnum.datasetQuote}
valueDesc={datasetQuoteValueDesc}
@@ -320,7 +320,7 @@ const SettingQuotePrompt = (props: RenderInputProps) => {
/>
</MyTooltip>
</Flex>
<Box mt={1}>
<Box mt={3}>
<Reference {...props} />
</Box>

View File

@@ -7,12 +7,14 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent
import { useCreation } from 'ahooks';
import { AppContext } from '@/pages/app/detail/components/context';
import { getEditorVariables } from '../../../../../utils';
import { WorkflowNodeEdgeContext } from '../../../../../context/workflowInitContext';
const TextInputRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const { nodeList, edges, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
// get variable
const variables = useCreation(() => {

View File

@@ -7,11 +7,12 @@ import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponent
import { useCreation } from 'ahooks';
import { AppContext } from '@/pages/app/detail/components/context';
import { getEditorVariables } from '../../../../../utils';
import { WorkflowNodeEdgeContext } from '../../../../../context/workflowInitContext';
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const { appDetail } = useContextSelector(AppContext, (v) => v);

View File

@@ -1,5 +1,5 @@
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import React, { useMemo } from 'react';
import React from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Flex } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
@@ -13,44 +13,40 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
const { t } = useTranslation();
const { label = '', description, valueType, valueDesc } = output;
const Render = useMemo(() => {
return (
<Box position={'relative'}>
<Flex
className="nodrag"
cursor={'default'}
alignItems={'center'}
fontWeight={'medium'}
color={'myGray.600'}
{...(output.type === FlowNodeOutputTypeEnum.source
? {
flexDirection: 'row-reverse'
}
: {})}
return (
<Box position={'relative'}>
<Flex
className="nodrag"
cursor={'default'}
alignItems={'center'}
fontWeight={'medium'}
color={'myGray.600'}
{...(output.type === FlowNodeOutputTypeEnum.source
? {
flexDirection: 'row-reverse'
}
: {})}
>
<Box
position={'relative'}
mr={1}
ml={output.type === FlowNodeOutputTypeEnum.source ? 1 : 0}
>
<Box
position={'relative'}
mr={1}
ml={output.type === FlowNodeOutputTypeEnum.source ? 1 : 0}
>
{t(label as any)}
</Box>
{description && <QuestionTip ml={1} label={t(description as any)} />}
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
</Flex>
{output.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', output.key)}
translate={[26, 0]}
position={Position.Right}
/>
)}
</Box>
);
}, [output.type, output.key, t, label, description, valueType, valueDesc, nodeId]);
return Render;
{t(label as any)}
</Box>
{description && <QuestionTip ml={1} label={t(description as any)} />}
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
</Flex>
{output.type === FlowNodeOutputTypeEnum.source && (
<SourceHandle
nodeId={nodeId}
handleId={getHandleId(nodeId, 'source', output.key)}
translate={[34, 0]}
position={Position.Right}
/>
)}
</Box>
);
};
export default React.memo(OutputLabel);

View File

@@ -139,7 +139,7 @@ const RenderOutput = ({
<FormLabel
key={output.key}
required={output.required}
mb={i === renderOutputs.length - 1 ? 0 : 5}
mb={i === renderOutputs.length - 1 ? 0 : 4}
position={'relative'}
>
<OutputLabel nodeId={nodeId} output={output} />

View File

@@ -2,6 +2,9 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import React from 'react';
import { DefaultEdgeOptions } from 'reactflow';
export const minZoom = 0.1;
export const maxZoom = 1.5;
export const connectionLineStyle: React.CSSProperties = {
strokeWidth: 2,
stroke: '#487FFF'

View File

@@ -15,7 +15,7 @@ import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/wor
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect, useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
import { useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
import React, {
Dispatch,
SetStateAction,
@@ -25,30 +25,47 @@ import React, {
useRef,
useState
} from 'react';
import {
Edge,
EdgeChange,
Node,
NodeChange,
OnConnectStartParams,
useEdgesState,
useNodesState
} from 'reactflow';
import { Edge, Node, OnConnectStartParams, ReactFlowProvider, useReactFlow } from 'reactflow';
import { createContext, useContextSelector } from 'use-context-selector';
import { defaultRunningStatus } from './constants';
import { defaultRunningStatus } from '../constants';
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { AppContext } from '@/pages/app/detail/components/context';
import ChatTest from './Flow/ChatTest';
import ChatTest from '../Flow/ChatTest';
import { useDisclosure } from '@chakra-ui/react';
import { uiWorkflow2StoreWorkflow } from './utils';
import { uiWorkflow2StoreWorkflow } from '../utils';
import { useTranslation } from 'next-i18next';
import { formatTime2YMDHMS, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
import { cloneDeep } from 'lodash';
import { SetState } from 'ahooks/lib/createUseStorageState';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import WorkflowInitContextProvider, { WorkflowNodeEdgeContext } from './workflowInitContext';
import WorkflowEventContextProvider from './workflowEventContext';
/*
Context
1. WorkflowInitContext: nodes
2. WorkflowNodeEdgeContext: 除了 nodes nodes edges
3. WorkflowContextProvider: 旧的 context
4. WorkflowEventContextProvider event
*/
export const ReactFlowCustomProvider = ({
templates,
children
}: {
templates: FlowNodeTemplateType[];
children: React.ReactNode;
}) => {
return (
<ReactFlowProvider>
<WorkflowInitContextProvider>
<WorkflowContextProvider basicNodeTemplates={templates}>
<WorkflowEventContextProvider>{children}</WorkflowEventContextProvider>
</WorkflowContextProvider>
</WorkflowInitContextProvider>
</ReactFlowProvider>
);
};
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
@@ -64,33 +81,22 @@ type WorkflowContextType = {
appId?: string;
basicNodeTemplates: FlowNodeTemplateType[];
filterAppIds?: string[];
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
mouseInCanvas: boolean;
// nodes
nodes: Node<FlowNodeItemType, string | undefined>[];
nodeList: FlowNodeItemType[];
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
hasToolNode: boolean;
hoverNodeId?: string;
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
onUpdateNodeError: (node: string, isError: Boolean) => void;
onResetNode: (e: { id: string; node: FlowNodeTemplateType }) => void;
onChangeNode: (e: FlowNodeChangeProps) => void;
getNodeDynamicInputs: (nodeId: string) => FlowNodeInputItemType[];
// edges
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
onDelEdge: (e: {
nodeId: string;
sourceHandle?: string | undefined;
targetHandle?: string | undefined;
}) => void;
hoverEdgeId?: string;
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
onSwitchTmpVersion: (data: WorkflowSnapshotsType, customTitle: string) => boolean;
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => boolean;
@@ -101,6 +107,19 @@ type WorkflowContextType = {
undo: () => void;
canRedo: boolean;
canUndo: boolean;
pushPastSnapshot: ({
pastNodes,
pastEdges,
customTitle,
chatConfig,
isSaved
}: {
pastNodes: Node[];
pastEdges: Edge[];
customTitle?: string;
chatConfig: AppChatConfigType;
isSaved?: boolean;
}) => boolean;
// connect
connectingEdge?: OnConnectStartParams;
@@ -158,10 +177,6 @@ type WorkflowContextType = {
}) => Promise<void>;
onStopNodeDebug: () => void;
// version history
showHistoryModal: boolean;
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
// chat test
setWorkflowTestData: React.Dispatch<
React.SetStateAction<
@@ -172,15 +187,6 @@ type WorkflowContextType = {
| undefined
>
>;
//
workflowControlMode?: 'drag' | 'select';
setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void;
menu: {
top: number;
left: number;
} | null;
setMenu: (value: React.SetStateAction<{ top: number; left: number } | null>) => void;
};
type DebugDataType = {
@@ -197,32 +203,11 @@ export const WorkflowContext = createContext<WorkflowContextType>({
throw new Error('Function not implemented.');
},
basicNodeTemplates: [],
reactFlowWrapper: null,
nodes: [],
nodeList: [],
mouseInCanvas: false,
setNodes: function (
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
): void {
throw new Error('Function not implemented.');
},
onNodesChange: function (changes: NodeChange[]): void {
throw new Error('Function not implemented.');
},
hasToolNode: false,
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
onUpdateNodeError: function (node: string, isError: Boolean): void {
throw new Error('Function not implemented.');
},
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
throw new Error('Function not implemented.');
},
onEdgesChange: function (changes: EdgeChange[]): void {
throw new Error('Function not implemented.');
},
onResetNode: function (e: { id: string; node: FlowNodeTemplateType }): void {
throw new Error('Function not implemented.');
},
@@ -271,9 +256,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
onChangeNode: function (e: FlowNodeChangeProps): void {
throw new Error('Function not implemented.');
},
setHoverEdgeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
setWorkflowTestData: function (
value: React.SetStateAction<
{ nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] } | undefined
@@ -291,10 +273,6 @@ export const WorkflowContext = createContext<WorkflowContextType>({
| undefined {
throw new Error('Function not implemented.');
},
showHistoryModal: false,
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
throw new Error('Function not implemented.');
},
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
throw new Error('Function not implemented.');
},
@@ -311,18 +289,27 @@ export const WorkflowContext = createContext<WorkflowContextType>({
},
canRedo: false,
canUndo: false,
workflowControlMode: 'drag',
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
throw new Error('Function not implemented.');
},
onSwitchTmpVersion: function (data: WorkflowSnapshotsType, customTitle: string): boolean {
throw new Error('Function not implemented.');
},
onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean {
throw new Error('Function not implemented.');
},
menu: null,
setMenu: function (value: React.SetStateAction<{ top: number; left: number } | null>): void {
pushPastSnapshot: function ({
pastNodes,
pastEdges,
customTitle,
chatConfig,
isSaved
}: {
pastNodes: Node[];
pastEdges: Edge[];
customTitle?: string;
chatConfig: AppChatConfigType;
isSaved?: boolean;
}): boolean {
throw new Error('Function not implemented.');
}
});
@@ -336,39 +323,14 @@ const WorkflowContextProvider = ({
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
const appId = appDetail._id;
const [workflowControlMode, setWorkflowControlMode] = useLocalStorageState<'drag' | 'select'>(
'workflow-control-mode',
{
defaultValue: 'drag',
listenStorageChange: true
}
);
// Mouse in canvas
const [mouseInCanvas, setMouseInCanvas] = useState(false);
useEffect(() => {
const handleMouseInCanvas = (e: MouseEvent) => {
setMouseInCanvas(true);
};
const handleMouseOutCanvas = (e: MouseEvent) => {
setMouseInCanvas(false);
};
reactFlowWrapper?.current?.addEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.addEventListener('mouseleave', handleMouseOutCanvas);
return () => {
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
};
}, [reactFlowWrapper?.current]);
/* edge */
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [hoverEdgeId, setHoverEdgeId] = useState<string>();
const edges = useContextSelector(WorkflowNodeEdgeContext, (state) => state.edges);
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (state) => state.setEdges);
const onDelEdge = useCallback(
({
nodeId,
@@ -396,32 +358,17 @@ const WorkflowContextProvider = ({
const [connectingEdge, setConnectingEdge] = useState<OnConnectStartParams>();
/* node */
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
const [hoverNodeId, setHoverNodeId] = useState<string>();
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (state) => state.setNodes);
const getNodes = useContextSelector(WorkflowNodeEdgeContext, (state) => state.getNodes);
const nodeListString = useContextSelector(
WorkflowNodeEdgeContext,
(state) => state.nodeListString
);
const nodeList = useMemo(
() => JSON.parse(nodeListString) as FlowNodeItemType[],
[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 !!nodeList.find((node) => node.flowNodeType === FlowNodeTypeEnum.tools);
}, [nodeList]);
@@ -483,9 +430,13 @@ const WorkflowContextProvider = ({
item.key === props.key ? props.value : item
);
} else if (type === 'replaceInput') {
updateObj.inputs = updateObj.inputs.map((item) =>
item.key === props.key ? props.value : item
);
if (!updateObj.inputs.find((item) => item.key === props.key)) {
updateObj.inputs.push(props.value);
} else {
updateObj.inputs = updateObj.inputs.map((item) =>
item.key === props.key ? props.value : item
);
}
} else if (type === 'addInput') {
const input = node.data.inputs.find((input) => input.key === props.value.key);
if (input) {
@@ -568,7 +519,9 @@ const WorkflowContextProvider = ({
);
/* ui flow to store data */
const { fitView } = useReactFlow();
const flowData2StoreDataAndCheck = useMemoizedFn((hideTip = false) => {
const nodes = getNodes();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {
@@ -577,6 +530,12 @@ const WorkflowContextProvider = ({
return storeWorkflow;
} else if (!hideTip) {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
// View move to the node that failed
fitView({
nodes: nodes.filter((node) => checkResults.includes(node.data.nodeId))
});
toast({
status: 'warning',
title: t('common:core.workflow.Check Failed')
@@ -585,6 +544,7 @@ const WorkflowContextProvider = ({
});
const flowData2StoreData = useMemoizedFn(() => {
const nodes = getNodes();
return uiWorkflow2StoreWorkflow({ nodes, edges });
});
@@ -884,22 +844,6 @@ const WorkflowContextProvider = ({
});
});
// Auto save snapshot
useDebounceEffect(
() => {
if (nodes.length === 0 || !appDetail.chatConfig) return;
pushPastSnapshot({
pastNodes: nodes,
pastEdges: edges,
customTitle: formatTime2YMDHMS(new Date()),
chatConfig: appDetail.chatConfig
});
},
[nodes, edges, appDetail.chatConfig],
{ wait: 500 }
);
const undo = useMemoizedFn(() => {
if (past[1]) {
setFuture((future) => [past[0], ...future]);
@@ -970,88 +914,80 @@ const WorkflowContextProvider = ({
]
);
/* Version histories */
const [showHistoryModal, setShowHistoryModal] = useState(false);
const value = useMemo(
() => ({
appId,
basicNodeTemplates,
/* event bus */
useEffect(() => {
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
eventBus.emit(EventNameEnum.receiveWorkflowStore, {
nodes,
edges
});
});
return () => {
eventBus.off(EventNameEnum.requestWorkflowStore);
};
}, [edges, nodes]);
// node
nodeList,
hasToolNode,
onUpdateNodeError,
onResetNode,
onChangeNode,
getNodeDynamicInputs,
const [menu, setMenu] = useState<{ top: number; left: number } | null>(null);
// edge
connectingEdge,
setConnectingEdge,
onDelEdge,
const value = {
appId,
reactFlowWrapper,
basicNodeTemplates,
workflowControlMode,
setWorkflowControlMode,
mouseInCanvas,
// snapshots
past,
setPast,
future,
undo,
redo,
canUndo: past.length > 1,
canRedo: !!future.length,
onSwitchTmpVersion,
onSwitchCloudVersion,
pushPastSnapshot,
// node
nodes,
setNodes,
onNodesChange,
nodeList,
hasToolNode,
hoverNodeId,
setHoverNodeId,
onUpdateNodeError,
onResetNode,
onChangeNode,
getNodeDynamicInputs,
// function
splitToolInputs,
initData,
flowData2StoreDataAndCheck,
flowData2StoreData,
// edge
edges,
setEdges,
hoverEdgeId,
setHoverEdgeId,
onEdgesChange,
connectingEdge,
setConnectingEdge,
onDelEdge,
// debug
workflowDebugData,
onNextNodeDebug,
onStartNodeDebug,
onStopNodeDebug,
// snapshots
past,
setPast,
future,
undo,
redo,
canUndo: past.length > 1,
canRedo: !!future.length,
onSwitchTmpVersion,
onSwitchCloudVersion,
// function
splitToolInputs,
initData,
flowData2StoreDataAndCheck,
flowData2StoreData,
// debug
workflowDebugData,
onNextNodeDebug,
onStartNodeDebug,
onStopNodeDebug,
// version history
showHistoryModal,
setShowHistoryModal,
// chat test
setWorkflowTestData,
menu,
setMenu
};
// chat test
setWorkflowTestData
}),
[
appId,
basicNodeTemplates,
connectingEdge,
flowData2StoreData,
flowData2StoreDataAndCheck,
future,
getNodeDynamicInputs,
hasToolNode,
initData,
nodeList,
onChangeNode,
onDelEdge,
onNextNodeDebug,
onResetNode,
onStartNodeDebug,
onStopNodeDebug,
onSwitchCloudVersion,
onSwitchTmpVersion,
onUpdateNodeError,
past,
pushPastSnapshot,
redo,
setPast,
splitToolInputs,
undo,
workflowDebugData
]
);
return (
<WorkflowContext.Provider value={value}>
@@ -1060,18 +996,4 @@ const WorkflowContextProvider = ({
</WorkflowContext.Provider>
);
};
export default WorkflowContextProvider;
type GetWorkflowStoreResponse = {
nodes: Node<FlowNodeItemType>[];
edges: Edge<any>[];
};
export const getWorkflowStore = () =>
new Promise<GetWorkflowStoreResponse>((resolve) => {
eventBus.on(EventNameEnum.receiveWorkflowStore, (data: GetWorkflowStoreResponse) => {
resolve(data);
eventBus.off(EventNameEnum.receiveWorkflowStore);
});
eventBus.emit(EventNameEnum.requestWorkflowStore);
});
export default React.memo(WorkflowContextProvider);

View File

@@ -0,0 +1,119 @@
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { createContext } from 'use-context-selector';
import { useLocalStorageState } from 'ahooks';
import { SetState } from 'ahooks/lib/createUseStorageState';
type WorkflowEventContextType = {
mouseInCanvas: boolean;
reactFlowWrapper: React.RefObject<HTMLDivElement> | null;
hoverNodeId?: string;
setHoverNodeId: React.Dispatch<React.SetStateAction<string | undefined>>;
hoverEdgeId?: string;
setHoverEdgeId: React.Dispatch<React.SetStateAction<string | undefined>>;
workflowControlMode?: 'drag' | 'select';
setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void;
menu: {
top: number;
left: number;
} | null;
setMenu: (value: React.SetStateAction<{ top: number; left: number } | null>) => void;
// version history
showHistoryModal: boolean;
setShowHistoryModal: React.Dispatch<React.SetStateAction<boolean>>;
};
export const WorkflowEventContext = createContext<WorkflowEventContextType>({
mouseInCanvas: false,
reactFlowWrapper: null,
setHoverNodeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
setHoverEdgeId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
},
workflowControlMode: 'drag',
setWorkflowControlMode: function (value?: SetState<'drag' | 'select'> | undefined): void {
throw new Error('Function not implemented.');
},
menu: null,
setMenu: function (value: React.SetStateAction<{ top: number; left: number } | null>): void {
throw new Error('Function not implemented.');
},
showHistoryModal: false,
setShowHistoryModal: function (value: React.SetStateAction<boolean>): void {
throw new Error('Function not implemented.');
}
});
const WorkflowEventContextProvider = ({ children }: { children: ReactNode }) => {
// Watch mouse in canvas
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [mouseInCanvas, setMouseInCanvas] = useState(false);
useEffect(() => {
const handleMouseInCanvas = (e: MouseEvent) => {
setMouseInCanvas(true);
};
const handleMouseOutCanvas = (e: MouseEvent) => {
setMouseInCanvas(false);
};
reactFlowWrapper?.current?.addEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.addEventListener('mouseleave', handleMouseOutCanvas);
return () => {
reactFlowWrapper?.current?.removeEventListener('mouseenter', handleMouseInCanvas);
reactFlowWrapper?.current?.removeEventListener('mouseleave', handleMouseOutCanvas);
};
}, [reactFlowWrapper?.current, setMouseInCanvas]);
// Watch hover node
const [hoverNodeId, setHoverNodeId] = useState<string>();
// Watch hover edge
const [hoverEdgeId, setHoverEdgeId] = useState<string>();
const [workflowControlMode, setWorkflowControlMode] = useLocalStorageState<'drag' | 'select'>(
'workflow-control-mode',
{
defaultValue: 'drag',
listenStorageChange: true
}
);
const [menu, setMenu] = useState<{ top: number; left: number } | null>(null);
/* Version histories */
const [showHistoryModal, setShowHistoryModal] = useState(false);
const contextValue = useMemo(
() => ({
mouseInCanvas,
reactFlowWrapper,
hoverNodeId,
setHoverNodeId,
hoverEdgeId,
setHoverEdgeId,
workflowControlMode,
setWorkflowControlMode,
menu,
setMenu,
showHistoryModal,
setShowHistoryModal
}),
[
mouseInCanvas,
hoverNodeId,
setHoverNodeId,
hoverEdgeId,
setHoverEdgeId,
workflowControlMode,
setWorkflowControlMode,
menu,
setMenu,
showHistoryModal,
setShowHistoryModal
]
);
return (
<WorkflowEventContext.Provider value={contextValue}>{children}</WorkflowEventContext.Provider>
);
};
export default WorkflowEventContextProvider;

View File

@@ -0,0 +1,138 @@
import { createContext } from 'use-context-selector';
import { postWorkflowDebug } from '@/web/core/workflow/api';
import {
checkWorkflowNodeAndConnection,
compareSnapshot,
storeEdgesRenderEdge,
storeNode2FlowNode
} from '@/web/core/workflow/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { FlowNodeItemType, StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { RuntimeEdgeItemType, StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { FlowNodeChangeProps } from '@fastgpt/global/core/workflow/type/fe';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useDebounceEffect, useLocalStorageState, useMemoizedFn, useUpdateEffect } from 'ahooks';
import React, {
Dispatch,
SetStateAction,
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import {
Edge,
EdgeChange,
Node,
NodeChange,
OnConnectStartParams,
useEdgesState,
useNodesState,
useReactFlow
} from 'reactflow';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
type WorkflowInitContextType = {
nodes: Node<FlowNodeItemType, string | undefined>[];
};
export const WorkflowInitContext = createContext<WorkflowInitContextType>({
nodes: []
});
type WorkflowActionContextType = {
setNodes: Dispatch<SetStateAction<Node<FlowNodeItemType, string | undefined>[]>>;
onNodesChange: OnChange<NodeChange>;
getNodes: () => Node<FlowNodeItemType, string | undefined>[];
nodeListString: string;
edges: Edge<any>[];
setEdges: Dispatch<SetStateAction<Edge<any>[]>>;
onEdgesChange: OnChange<EdgeChange>;
};
export const WorkflowNodeEdgeContext = createContext<WorkflowActionContextType>({
setNodes: function (
value: React.SetStateAction<Node<FlowNodeItemType, string | undefined>[]>
): void {
throw new Error('Function not implemented.');
},
onNodesChange: function (changes: NodeChange[]): void {
throw new Error('Function not implemented.');
},
getNodes: function (): Node<FlowNodeItemType, string | undefined>[] {
throw new Error('Function not implemented.');
},
nodeListString: JSON.stringify([]),
edges: [],
setEdges: function (value: React.SetStateAction<Edge<any>[]>): void {
throw new Error('Function not implemented.');
},
onEdgesChange: function (changes: EdgeChange[]): void {
throw new Error('Function not implemented.');
}
});
const WorkflowInitContextProvider = ({ children }: { children: ReactNode }) => {
// Nodes
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
const getNodes = useMemoizedFn(() => nodes);
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
const nodeList = useMemo(
() => JSON.parse(nodeListString) as FlowNodeItemType[],
[nodeListString]
);
// Edges
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Elevate childNodes
useEffect(() => {
setNodes((nodes) =>
nodes.map((node) => (node.data.parentNodeId ? { ...node, zIndex: 1001 } : node))
);
}, [nodeList]);
// Elevate edges of childNodes
useEffect(() => {
setEdges((state) =>
state.map((item) =>
nodeList.some((node) => item.source === node.nodeId && node.parentNodeId)
? { ...item, zIndex: 1001 }
: item
)
);
}, [edges.length]);
const actionContextValue = useMemo(
() => ({
setNodes,
onNodesChange,
getNodes,
nodeListString,
edges,
setEdges,
onEdgesChange
}),
[setNodes, onNodesChange, getNodes, nodeListString, edges, setEdges, onEdgesChange]
);
return (
<WorkflowInitContext.Provider
value={{
nodes
}}
>
<WorkflowNodeEdgeContext.Provider value={actionContextValue}>
{children}
</WorkflowNodeEdgeContext.Provider>
</WorkflowInitContext.Provider>
);
};
export default WorkflowInitContextProvider;

View File

@@ -1,4 +1,4 @@
import { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react';
import { Dispatch, ReactNode, SetStateAction, useCallback, useMemo, useState } from 'react';
import { createContext } from 'use-context-selector';
import { defaultApp } from '@/web/core/app/constants';
import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api';
@@ -186,22 +186,39 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
[appDetail.name, deleteApp, openConfirmDel, t]
);
const contextValue: AppContextType = {
appId,
currentTab,
route2Tab,
appDetail,
setAppDetail,
loadingApp,
updateAppDetail,
onOpenInfoEdit,
onOpenTeamTagModal,
onDelApp,
onSaveApp,
appLatestVersion,
reloadAppLatestVersion,
reloadApp
};
const contextValue: AppContextType = useMemo(
() => ({
appId,
currentTab,
route2Tab,
appDetail,
setAppDetail,
loadingApp,
updateAppDetail,
onOpenInfoEdit,
onOpenTeamTagModal,
onDelApp,
onSaveApp,
appLatestVersion,
reloadAppLatestVersion,
reloadApp
}),
[
appDetail,
appId,
appLatestVersion,
currentTab,
loadingApp,
onDelApp,
onOpenInfoEdit,
onOpenTeamTagModal,
onSaveApp,
reloadApp,
reloadAppLatestVersion,
route2Tab,
updateAppDetail
]
);
return (
<AppContext.Provider value={contextValue}>

View File

@@ -113,6 +113,9 @@ export const useChatTest = ({
appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
chatType="chat"
showRawSource
showNodeStatus
chatConfig={chatConfig}
onStartChat={startChat}
onDelMessage={() => {}}

View File

@@ -249,7 +249,9 @@ const ListItem = () => {
{isPc && (
<HStack spacing={0.5} className="time">
<MyIcon name={'history'} w={'0.85rem'} color={'myGray.400'} />
<Box color={'myGray.500'}>{formatTimeToChatTime(app.updateTime)}</Box>
<Box color={'myGray.500'}>
{t(formatTimeToChatTime(app.updateTime) as any).replace('#', ':')}
</Box>
</HStack>
)}
{(AppFolderTypeList.includes(app.type)

View File

@@ -13,12 +13,14 @@ import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
type HistoryItemType = {
id: string;
title: string;
customTitle?: string;
top?: boolean;
updateTime: Date;
};
const ChatHistorySlider = ({
@@ -59,11 +61,18 @@ const ChatHistorySlider = ({
const concatHistory = useMemo(() => {
const formatHistories: HistoryItemType[] = histories.map((item) => {
return { id: item.chatId, title: item.title, customTitle: item.customTitle, top: item.top };
return {
id: item.chatId,
title: item.title,
customTitle: item.customTitle,
top: item.top,
updateTime: item.updateTime
};
});
const newChat: HistoryItemType = {
id: activeChatId,
title: t('common:core.chat.New Chat')
title: t('common:core.chat.New Chat'),
updateTime: new Date()
};
const activeChat = histories.find((item) => item.chatId === activeChatId);
@@ -188,7 +197,10 @@ const ChatHistorySlider = ({
_hover={{
bg: 'myGray.50',
'& .more': {
visibility: 'visible'
display: 'block'
},
'& .time': {
display: isPc ? 'none' : 'block'
}
}}
bg={item.top ? '#E6F6F6 !important' : ''}
@@ -214,69 +226,80 @@ const ChatHistorySlider = ({
{item.customTitle || item.title}
</Box>
{!!item.id && (
<Box className="more" visibility={['visible', 'hidden']}>
<MyMenu
Button={
<IconButton
size={'xs'}
variant={'whiteBase'}
icon={<MyIcon name={'more'} w={'14px'} p={1} />}
aria-label={''}
/>
}
menuList={[
{
children: [
...(onSetHistoryTop
? [
{
label: item.top
? t('common:core.chat.Unpin')
: t('common:core.chat.Pin'),
icon: 'core/chat/setTopLight',
onClick: () => {
onSetHistoryTop({
chatId: item.id,
top: !item.top
});
}
}
]
: []),
...(onSetCustomTitle
? [
{
label: t('common:common.Custom Title'),
icon: 'common/customTitleLight',
onClick: () => {
onOpenModal({
defaultVal: item.customTitle || item.title,
onSuccess: (e) =>
onSetCustomTitle({
chatId: item.id,
title: e
})
});
}
}
]
: []),
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
onDelHistory({ chatId: item.id });
if (item.id === activeChatId) {
onChangeChatId();
}
},
type: 'danger'
}
]
<Flex gap={2} alignItems={'center'}>
<Box
className="time"
display={'block'}
fontWeight={'400'}
fontSize={'mini'}
color={'myGray.500'}
>
{t(formatTimeToChatTime(item.updateTime) as any).replace('#', ':')}
</Box>
<Box className="more" display={['block', 'none']}>
<MyMenu
Button={
<IconButton
size={'xs'}
variant={'whiteBase'}
icon={<MyIcon name={'more'} w={'14px'} p={1} />}
aria-label={''}
/>
}
]}
/>
</Box>
menuList={[
{
children: [
...(onSetHistoryTop
? [
{
label: item.top
? t('common:core.chat.Unpin')
: t('common:core.chat.Pin'),
icon: 'core/chat/setTopLight',
onClick: () => {
onSetHistoryTop({
chatId: item.id,
top: !item.top
});
}
}
]
: []),
...(onSetCustomTitle
? [
{
label: t('common:common.Custom Title'),
icon: 'common/customTitleLight',
onClick: () => {
onOpenModal({
defaultVal: item.customTitle || item.title,
onSuccess: (e) =>
onSetCustomTitle({
chatId: item.id,
title: e
})
});
}
}
]
: []),
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
onDelHistory({ chatId: item.id });
if (item.id === activeChatId) {
onChangeChatId();
}
},
type: 'danger'
}
]
}
]}
/>
</Box>
</Flex>
)}
</Flex>
))}

View File

@@ -34,7 +34,7 @@ import { useChat } from '@/components/core/chat/ChatContainer/useChat';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { InitChatResponse } from '@/global/core/chat/api';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
const CustomPluginRunBox = dynamic(() => import('./components/CustomPluginRunBox'));
@@ -284,6 +284,9 @@ const Chat = ({
onDelMessage={({ contentId }) => delChatRecordById({ contentId, appId, chatId })}
appId={appId}
chatId={chatId}
chatType={'chat'}
showRawSource
showNodeStatus
/>
)}
</Box>
@@ -342,7 +345,7 @@ const Render = (props: Props) => {
}
});
const providerParams = useMemo(() => ({ appId }), [appId]);
const providerParams = useMemo(() => ({ appId, source: ChatSourceEnum.online }), [appId]);
return (
<ChatContextProvider params={providerParams}>
<Chat {...props} myApps={myApps} />

View File

@@ -41,21 +41,26 @@ type Props = {
appAvatar: string;
shareId: string;
authToken: string;
customUid: string;
showRawSource: boolean;
showNodeStatus: boolean;
};
const OutLink = ({
outLinkUid
}: Props & {
outLinkUid: string;
}) => {
const OutLink = (
props: Props & {
outLinkUid: string;
}
) => {
const { t } = useTranslation();
const router = useRouter();
const { outLinkUid, showRawSource, showNodeStatus } = props;
const {
shareId = '',
chatId = '',
showHistory = '1',
showHead = '1',
authToken,
customUid,
...customVariables
} = router.query as {
shareId: string;
@@ -295,6 +300,7 @@ const OutLink = ({
return (
<>
<NextHead title={props.appName || 'AI'} desc={props.appIntro} icon={props.appAvatar} />
<PageContainer
isLoading={loading}
{...(isEmbed
@@ -360,6 +366,9 @@ const OutLink = ({
chatId={chatId}
shareId={shareId}
outLinkUid={outLinkUid}
chatType="share"
showRawSource={showRawSource}
showNodeStatus={showNodeStatus}
/>
)}
</Box>
@@ -371,13 +380,13 @@ const OutLink = ({
};
const Render = (props: Props) => {
const { shareId, authToken } = props;
const { shareId, authToken, customUid } = props;
const { localUId, loaded } = useShareChatStore();
const [isLoaded, setIsLoaded] = useState(false);
const contextParams = useMemo(() => {
return { shareId, outLinkUid: authToken || localUId };
}, [authToken, localUId, shareId]);
return { shareId, outLinkUid: authToken || localUId || customUid };
}, [authToken, customUid, localUId, shareId]);
useMount(() => {
setIsLoaded(true);
@@ -386,11 +395,12 @@ const Render = (props: Props) => {
return (
<>
<NextHead title={props.appName || 'AI'} desc={props.appIntro} icon={props.appAvatar} />
{systemLoaded && (
{systemLoaded ? (
<ChatContextProvider params={contextParams}>
<OutLink {...props} outLinkUid={contextParams.outLinkUid} />;
</ChatContextProvider>
) : (
<NextHead title="Loading..." />
)}
</>
);
@@ -401,6 +411,7 @@ export default React.memo(Render);
export async function getServerSideProps(context: any) {
const shareId = context?.query?.shareId || '';
const authToken = context?.query?.authToken || '';
const customUid = context?.query?.customUid || '';
const app = await (async () => {
try {
@@ -409,7 +420,7 @@ export async function getServerSideProps(context: any) {
{
shareId
},
'appId'
'appId showRawSource showNodeStatus'
)
.populate('appId', 'name avatar intro')
.lean()) as OutLinkWithAppType;
@@ -422,11 +433,14 @@ export async function getServerSideProps(context: any) {
return {
props: {
appName: app?.appId?.name ?? 'name',
appName: app?.appId?.name ?? 'AI',
appAvatar: app?.appId?.avatar ?? '',
appIntro: app?.appId?.intro ?? 'intro',
appIntro: app?.appId?.intro ?? 'AI',
showRawSource: app?.showRawSource ?? false,
showNodeStatus: app?.showNodeStatus ?? false,
shareId: shareId ?? '',
authToken: authToken ?? '',
customUid,
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow']))
}
};

View File

@@ -296,6 +296,9 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
chatId={chatId}
teamId={teamId}
teamToken={teamToken}
chatType="team"
showRawSource
showNodeStatus
/>
)}
</Box>

View File

@@ -54,7 +54,7 @@ const WebsiteConfigModal = ({
{t('common:core.dataset.website.Config Description')}
{feConfigs?.docUrl && (
<Link
href={getDocPath('/docs/course/websync')}
href={getDocPath('/docs/guide/knowledge_base/websync/')}
target="_blank"
textDecoration={'underline'}
fontWeight={'bold'}

View File

@@ -1,12 +1,7 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import {
Box,
Flex,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Input,
Button,
ModalBody,
@@ -29,6 +24,8 @@ import { useContextSelector } from 'use-context-selector';
import { DatasetImportContext } from '../Context';
import { useToast } from '@fastgpt/web/hooks/useToast';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean }) {
const { t } = useTranslation();
@@ -127,14 +124,7 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
<Box>
<Flex alignItems={'center'}>
<Box>{t('dataset:ideal_chunk_length')}</Box>
<MyTooltip label={t('dataset:ideal_chunk_length_tips')}>
<MyIcon
name={'common/questionLight'}
ml={1}
w={'14px'}
color={'myGray.500'}
/>
</MyTooltip>
<QuestionTip label={t('dataset:ideal_chunk_length_tips')} />
</Flex>
<Box
mt={1}
@@ -150,30 +140,18 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
max: maxChunkSize
})}
>
<NumberInput
size={'sm'}
step={100}
<MyNumberInput
name={chunkSizeField}
min={minChunkSize}
max={maxChunkSize}
size={'sm'}
step={100}
value={chunkSize}
onChange={(e) => {
if (e === undefined) return;
setValue(chunkSizeField, +e);
}}
>
<NumberInputField
min={minChunkSize}
max={maxChunkSize}
{...register(chunkSizeField, {
min: minChunkSize,
max: maxChunkSize,
valueAsNumber: true
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
/>
</MyTooltip>
</Box>
</Box>
@@ -182,14 +160,9 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
<Box mt={3}>
<Box>
{t('common:core.dataset.import.Custom split char')}
<MyTooltip label={t('common:core.dataset.import.Custom split char Tips')}>
<MyIcon
name={'common/questionLight'}
ml={1}
w={'14px'}
color={'myGray.500'}
/>
</MyTooltip>
<QuestionTip
label={t('common:core.dataset.import.Custom split char Tips')}
/>
</Box>
<Box mt={1}>
<Input

View File

@@ -80,7 +80,10 @@ const CustomLinkImport = () => {
{t('common:core.dataset.website.Selector')}
<Box color={'myGray.500'} fontSize={'sm'}>
{feConfigs?.docUrl && (
<Link href={getDocPath('/docs/course/websync/#选择器如何使用')} target="_blank">
<Link
href={getDocPath('/docs/guide/knowledge_base/websync/#选择器如何使用')}
target="_blank"
>
{t('common:core.dataset.website.Selector Course')}
</Link>
)}

View File

@@ -26,11 +26,9 @@ import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { getDefaultIndex, getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { DatasetDataIndexItemType } from '@fastgpt/global/core/dataset/type';
import SideTabs from '@/components/SideTabs';
import DeleteIcon from '@fastgpt/web/components/common/Icon/delete';
import { defaultCollectionDetail } from '@/web/core/dataset/constants';
import { getDocPath } from '@/web/common/system/doc';
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -101,7 +99,9 @@ const InputDataModal = ({
mr={'0.38rem'}
color={'myGray.500'}
ml={1}
onClick={() => window.open(getDocPath('/docs/course/dataset_engine'), '_blank')}
onClick={() =>
window.open(getDocPath('/docs/guide/knowledge_base/dataset_engine/'), '_blank')
}
_hover={{
color: 'primary.600',
cursor: 'pointer'

View File

@@ -18,7 +18,10 @@ const MetaDataCard = ({ datasetId }: { datasetId: string }) => {
collectionId: string;
datasetId: string;
};
const readSource = getCollectionSourceAndOpen(collectionId);
const readSource = getCollectionSourceAndOpen({
collectionId
});
const { data: collection, loading: isLoading } = useRequest2(
() => getDatasetCollectionById(collectionId),
{

View File

@@ -89,14 +89,12 @@ const NavBar = ({ currentTab }: { currentTab: TabEnum }) => {
<IconButton
p={2}
mr={2}
w={'1.5rem'}
h={'24px'}
border={'1px solid'}
borderColor={'myGray.200'}
boxShadow={'1'}
icon={<MyIcon name={'common/arrowLeft'} w={'16px'} color={'myGray.500'} />}
bg={'white'}
size={'smSquare'}
size={'xsSquare'}
borderRadius={'50%'}
aria-label={''}
_hover={'none'}

View File

@@ -382,9 +382,7 @@ const TestHistories = React.memo(function TestHistories({
{item.text}
</Box>
<Box flex={'0 0 70px'}>
{formatTimeToChatTime(item.time).includes('.')
? t(formatTimeToChatTime(item.time) as any)
: formatTimeToChatTime(item.time)}
{t(formatTimeToChatTime(item.time) as any).replace('#', ':')}
</Box>
<MyTooltip label={t('common:core.dataset.test.delete test history')}>
<Box w={'14px'} h={'14px'}>

View File

@@ -1,5 +1,5 @@
import React, { useState, Dispatch, useCallback } from 'react';
import { FormControl, Box, Input, Button, useDisclosure } from '@chakra-ui/react';
import React, { Dispatch } from 'react';
import { FormControl, Box, Input, Button } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postFindPassword } from '@/web/support/user/api';
@@ -73,11 +73,11 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
<Box fontWeight={'medium'} fontSize={'lg'} textAlign={'center'} color={'myGray.900'}>
{t('user:password.retrieved_account', { account: feConfigs?.systemTitle })}
</Box>
<Box
mt={'42px'}
mt={9}
onKeyDown={(e) => {
if (e.keyCode === 13 && !e.shiftKey && !requesting) {
handleSubmit(onclickFindPassword)();
@@ -87,6 +87,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl isInvalid={!!errors.username}>
<Input
bg={'myGray.50'}
size={'lg'}
placeholder={placeholder}
{...register('username', {
required: t('user:password.email_phone_void'),
@@ -107,6 +108,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
>
<Input
bg={'myGray.50'}
size={'lg'}
flex={1}
maxLength={8}
placeholder={t('user:password.verification_code')}
@@ -120,6 +122,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Input
bg={'myGray.50'}
type={'password'}
size={'lg'}
placeholder={t('user:password.new_password')}
{...register('password', {
required: t('user:password.password_required'),
@@ -138,6 +141,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Input
bg={'myGray.50'}
type={'password'}
size={'lg'}
placeholder={t('user:password.confirm')}
{...register('password2', {
validate: (val) =>
@@ -148,9 +152,12 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Button
type="submit"
mt={10}
mt={12}
w={'100%'}
size={['md', 'md']}
rounded={['md', 'md']}
h={[10, 10]}
fontWeight={['medium', 'medium']}
colorScheme="blue"
isLoading={requesting}
onClick={handleSubmit(onclickFindPassword)}
@@ -159,9 +166,10 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
</Button>
<Box
float={'right'}
fontSize="sm"
mt={2}
fontSize="mini"
mt={3}
mb={'50px'}
fontWeight={'medium'}
color={'primary.700'}
cursor={'pointer'}
_hover={{ textDecoration: 'underline' }}

View File

@@ -80,7 +80,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
return (
<FormLayout setPageType={setPageType} pageType={LoginPageTypeEnum.passwordLogin}>
<Box
mt={'42px'}
mt={9}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !requesting) {
handleSubmit(onclickLogin)();
@@ -90,15 +90,17 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl isInvalid={!!errors.username}>
<Input
bg={'myGray.50'}
size={'lg'}
placeholder={placeholder}
{...register('username', {
required: true
})}
></Input>
</FormControl>
<FormControl mt={6} isInvalid={!!errors.password}>
<FormControl mt={7} isInvalid={!!errors.password}>
<Input
bg={'myGray.50'}
size={'lg'}
type={'password'}
placeholder={
isCommunityVersion
@@ -115,13 +117,19 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
></Input>
</FormControl>
{feConfigs?.docUrl && (
<Flex alignItems={'center'} mt={7} fontSize={'mini'}>
<Flex
alignItems={'center'}
mt={7}
fontSize={'mini'}
color={'myGray.700'}
fontWeight={'medium'}
>
{t('login:policy_tip')}
<Link
ml={1}
href={getDocPath('/docs/agreement/terms/')}
target={'_blank'}
color={'primary.500'}
color={'primary.700'}
>
{t('login:terms')}
</Link>
@@ -129,7 +137,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
<Link
href={getDocPath('/docs/agreement/privacy/')}
target={'_blank'}
color={'primary.500'}
color={'primary.700'}
>
{t('login:privacy')}
</Link>
@@ -138,9 +146,11 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
<Button
type="submit"
my={6}
my={5}
w={'100%'}
size={['md', 'md']}
h={[10, 10]}
fontWeight={['medium', 'medium']}
colorScheme="blue"
isLoading={requesting}
onClick={handleSubmit(onclickLogin)}
@@ -148,29 +158,34 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
{t('login:Login')}
</Button>
<Flex align={'center'} justifyContent={'flex-end'} color={'primary.700'}>
<Flex
align={'center'}
justifyContent={'flex-end'}
color={'primary.700'}
fontWeight={'medium'}
>
{feConfigs?.find_password_method && feConfigs.find_password_method.length > 0 && (
<Box
cursor={'pointer'}
_hover={{ textDecoration: 'underline' }}
onClick={() => setPageType('forgetPassword')}
fontSize="sm"
fontSize="mini"
>
{t('login:forget_password')}
</Box>
)}
{feConfigs?.register_method && feConfigs.register_method.length > 0 && (
<>
<Box mx={3} h={'16px'} w={'1.5px'} bg={'myGray.250'}></Box>
<Flex alignItems={'center'}>
<Box mx={3} h={'12px'} w={'1px'} bg={'myGray.250'}></Box>
<Box
cursor={'pointer'}
_hover={{ textDecoration: 'underline' }}
onClick={() => setPageType('register')}
fontSize="sm"
fontSize="mini"
>
{t('login:register')}
</Box>
</>
</Flex>
)}
</Flex>
</Box>

View File

@@ -1,15 +1,15 @@
import React, { Dispatch } from 'react';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import type { ResLogin } from '@/global/support/api/userRes';
import { Box, Center, Image } from '@chakra-ui/react';
import { Box, Center } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { getWXLoginQR, getWXLoginResult } from '@/web/support/user/api';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRouter } from 'next/router';
import { useToast } from '@fastgpt/web/hooks/useToast';
import FormLayout from './components/FormLayout';
import { useTranslation } from 'next-i18next';
import Loading from '@fastgpt/web/components/common/MyLoading';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
interface Props {
loginSuccess: (e: ResLogin) => void;
@@ -40,12 +40,12 @@ const WechatForm = ({ setPageType, loginSuccess }: Props) => {
return (
<FormLayout setPageType={setPageType} pageType={LoginPageTypeEnum.wechat}>
<Box>
<Box w={'full'} textAlign={'center'} pt={5}>
<Box w={'full'} textAlign={'center'} pt={6} fontWeight={'medium'}>
{t('common:support.user.login.wx_qr_login')}
</Box>
<Box p={5} display={'flex'} w={'full'} justifyContent={'center'}>
{wechatInfo?.codeUrl ? (
<Image w="200px" src={wechatInfo?.codeUrl} alt="qrcode"></Image>
<MyImage w="200px" src={wechatInfo?.codeUrl} alt="qrcode"></MyImage>
) : (
<Center w={200} h={200} position={'relative'}>
<Loading fixed={false} />

View File

@@ -1,6 +1,6 @@
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AbsoluteCenter, Box, Button, Flex, Image } from '@chakra-ui/react';
import { AbsoluteCenter, Box, Button, Flex } from '@chakra-ui/react';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { OAuthEnum } from '@fastgpt/global/support/user/constant';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -8,9 +8,9 @@ import { customAlphabet } from 'nanoid';
import { useRouter } from 'next/router';
import { Dispatch, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import Divider from '@/pages/app/detail/components/WorkflowComponents/Flow/components/Divider';
import I18nLngSelector from '@/components/Select/I18nLngSelector';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 8);
interface Props {
@@ -64,7 +64,7 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
{
label: t('common:support.user.login.Password login'),
provider: LoginPageTypeEnum.passwordLogin,
icon: 'support/account/passwordLogin',
icon: 'support/permission/privateLight',
pageType: LoginPageTypeEnum.passwordLogin
}
]
@@ -77,20 +77,20 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
return (
<Flex flexDirection={'column'} h={'100%'}>
<Flex alignItems={'center'} justify={'space-between'}>
<Flex>
<Flex alignItems={'center'}>
<Flex
w={['48px', '56px']}
h={['48px', '56px']}
w={['42px', '56px']}
h={['42px', '56px']}
bg={'myGray.25'}
borderRadius={'xl'}
borderWidth={'1.5px'}
borderColor={'borderColor.base'}
borderRadius={['semilg', 'lg']}
borderWidth={['1px', '1.5px']}
borderColor={'myGray.200'}
alignItems={'center'}
justifyContent={'center'}
>
<Image src={LOGO_ICON} w={['24px', '28px']} alt={'icon'} />
<MyImage src={LOGO_ICON} w={['22.5px', '36px']} alt={'icon'} />
</Flex>
<Box ml={3} fontSize={['2xl', '3xl']} fontWeight={'bold'}>
<Box ml={[3, 5]} fontSize={['lg', 'xl']} fontWeight={'bold'} color={'myGray.900'}>
{feConfigs?.systemTitle}
</Box>
</Flex>
@@ -101,26 +101,21 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
<>
<Box flex={1} />
<Box position={'relative'}>
<Divider />
<AbsoluteCenter bg="white" px="4" color={'myGray.500'}>
<Box h={'1px'} bg={'myGray.250'} />
<AbsoluteCenter bg={'white'} px={3} color={'myGray.500'} fontSize={'mini'}>
or
</AbsoluteCenter>
</Box>
<Box mt={8}>
<Box mt={4}>
{oAuthList.map((item) => (
<Box key={item.provider} _notFirst={{ mt: 4 }}>
<Button
variant={'whitePrimary'}
w={'100%'}
h={'42px'}
leftIcon={
<MyIcon
name={item.icon as any}
w={'20px'}
cursor={'pointer'}
color={'myGray.800'}
/>
}
h={'40px'}
borderRadius={'sm'}
fontWeight={'medium'}
leftIcon={<MyIcon name={item.icon as any} w={'20px'} />}
onClick={() => {
item.redirectUrl &&
setLoginStore({
@@ -142,8 +137,9 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
<Button
variant={'whitePrimary'}
w={'100%'}
h={'42px'}
leftIcon={<Image alt="" src={feConfigs.sso.icon as any} w="20px" />}
h={'40px'}
borderRadius={'sm'}
leftIcon={<MyImage alt="" src={feConfigs.sso.icon as any} w="20px" />}
onClick={() => {
feConfigs.sso?.url && router.replace(feConfigs.sso?.url, '_self');
}}

View File

@@ -102,11 +102,11 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
<Box fontWeight={'medium'} fontSize={'lg'} textAlign={'center'} color={'myGray.900'}>
{t('user:register.register_account', { account: feConfigs?.systemTitle })}
</Box>
<Box
mt={'42px'}
mt={9}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !requesting) {
handleSubmit(onclickRegister)();
@@ -116,6 +116,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl isInvalid={!!errors.username}>
<Input
bg={'myGray.50'}
size={'lg'}
placeholder={placeholder}
{...register('username', {
required: t('user:password.email_phone_void'),
@@ -135,6 +136,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
position={'relative'}
>
<Input
size={'lg'}
bg={'myGray.50'}
flex={1}
maxLength={8}
@@ -148,6 +150,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl mt={6} isInvalid={!!errors.password}>
<Input
bg={'myGray.50'}
size={'lg'}
type={'password'}
placeholder={t('user:password.new_password')}
{...register('password', {
@@ -166,6 +169,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl mt={6} isInvalid={!!errors.password2}>
<Input
bg={'myGray.50'}
size={'lg'}
type={'password'}
placeholder={t('user:password.confirm')}
{...register('password2', {
@@ -176,9 +180,12 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
</FormControl>
<Button
type="submit"
mt={6}
mt={12}
w={'100%'}
size={['md', 'md']}
rounded={['md', 'md']}
h={[10, 10]}
fontWeight={['medium', 'medium']}
colorScheme="blue"
isLoading={requesting}
onClick={handleSubmit(onclickRegister)}
@@ -187,9 +194,10 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
</Button>
<Box
float={'right'}
fontSize="sm"
mt={2}
fontSize="mini"
mt={3}
mb={'50px'}
fontWeight={'medium'}
color={'primary.700'}
cursor={'pointer'}
_hover={{ textDecoration: 'underline' }}

Some files were not shown because too many files have changed in this diff Show More