Concat plugin to app (#1799)

This commit is contained in:
Archer
2024-06-19 14:38:21 +08:00
committed by GitHub
parent b17d14bb7d
commit 565bfc8486
220 changed files with 5018 additions and 4667 deletions

View File

@@ -29,10 +29,12 @@ export default function InputGuideBox({
const { data = [] } = useRequest2(
async () => {
if (!text) return [];
// More than 20 characters, it's basically meaningless
if (text.length > 20) return [];
return await queryChatInputGuideList(
{
appId,
searchKey: text.slice(0, 50),
searchKey: text,
...outLinkAuthData
},
chatInputGuide.customUrl ? chatInputGuide.customUrl : undefined

View File

@@ -79,7 +79,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
return (
<>
<Box h={'100%'} bg={'myGray.100'}>
{isPc === true && (
{isPc ? (
<>
{isHideNavbar ? (
<Auth>{children}</Auth>
@@ -94,8 +94,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
</>
)}
</>
)}
{isPc === false && (
) : (
<>
<Box h={'100%'} display={['block', 'none']}>
{phoneUnShowLayoutRoute[router.pathname] || isChatPage ? (

View File

@@ -40,13 +40,6 @@ const Navbar = ({ unread }: { unread: number }) => {
link: `/app/list`,
activeLink: ['/app/list', '/app/detail']
},
{
label: t('navbar.Plugin'),
icon: 'common/navbar/pluginLight',
activeIcon: 'common/navbar/pluginFill',
link: `/plugin/list`,
activeLink: ['/plugin/list', '/plugin/edit']
},
{
label: t('navbar.Datasets'),
icon: 'core/dataset/datasetLight',

View File

@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { Box, Flex, Grid, Image } from '@chakra-ui/react';
import type { GridProps } from '@chakra-ui/react';
import type { FlexProps, GridProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -9,10 +9,11 @@ interface Props extends GridProps {
list: { id: string; icon?: string; label: string | React.ReactNode }[];
activeId: string;
size?: 'sm' | 'md' | 'lg';
inlineStyles?: FlexProps;
onChange: (id: string) => void;
}
const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
const Tabs = ({ list, size = 'md', activeId, onChange, inlineStyles, ...props }: Props) => {
const { t } = useTranslation();
const sizeMap = useMemo(() => {
switch (size) {
@@ -55,6 +56,7 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
borderBottom={'2px solid transparent'}
px={3}
whiteSpace={'nowrap'}
{...inlineStyles}
{...(activeId === item.id
? {
color: 'primary.600',

View File

@@ -91,7 +91,7 @@ const MyRadio = ({
</Box>
)}
</Box>
<Radio isChecked={value === item.value} />
{!hiddenCircle && <Radio isChecked={value === item.value} />}
</Flex>
))}
</Grid>

View File

@@ -1,48 +0,0 @@
import React from 'react';
import { Flex, Box, BoxProps, border } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
type Props = BoxProps & {
list: {
icon?: string;
label: string | React.ReactNode;
value: string;
}[];
value: string;
onChange: (e: string) => void;
};
const RowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => {
return (
<Box display={'inline-flex'} px={'3px'} {...props}>
{list.map((item) => (
<Flex
key={item.value}
flex={'1 0 0'}
alignItems={'center'}
cursor={'pointer'}
px={px}
py={py}
userSelect={'none'}
whiteSpace={'noWrap'}
borderBottom={'2px solid'}
{...(value === item.value
? {
bg: 'white',
color: 'primary.600',
borderColor: 'primary.600'
}
: {
borderColor: 'myGray.100',
onClick: () => onChange(item.value)
})}
>
{item.icon && <MyIcon name={item.icon as any} mr={1} w={'14px'} />}
<Box fontSize={'sm'}>{item.label}</Box>
</Flex>
))}
</Box>
);
};
export default RowTabs;

View File

@@ -1,4 +1,4 @@
import { Box, Flex } from '@chakra-ui/react';
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import React, { useMemo } from 'react';
import { useTranslation } from 'next-i18next';
@@ -9,9 +9,17 @@ const FolderPath = (props: {
FirstPathDom?: React.ReactNode;
onClick: (parentId: string) => void;
fontSize?: string;
hoverStyle?: BoxProps;
}) => {
const { t } = useTranslation();
const { paths, rootName = t('common.folder.Root Path'), FirstPathDom, onClick, fontSize } = props;
const {
paths,
rootName = t('common.folder.Root Path'),
FirstPathDom,
onClick,
fontSize,
hoverStyle
} = props;
const concatPaths = useMemo(
() => [
@@ -43,9 +51,10 @@ const FolderPath = (props: {
}
: {
cursor: 'pointer',
color: 'myGray.600',
color: 'myGray.500',
_hover: {
bg: 'myGray.100'
bg: 'myGray.100',
...hoverStyle
},
onClick: () => {
onClick(item.parentId);

View File

@@ -13,10 +13,18 @@ const AppTypeTag = ({ type }: { type: AppTypeEnum }) => {
label: appT('type.Simple bot'),
icon: 'core/app/type/simple'
},
[AppTypeEnum.advanced]: {
[AppTypeEnum.workflow]: {
label: appT('type.Workflow bot'),
icon: 'core/app/type/workflow'
},
[AppTypeEnum.plugin]: {
label: appT('type.Plugin'),
icon: 'core/app/type/plugin'
},
[AppTypeEnum.httpPlugin]: {
label: appT('type.Http plugin'),
icon: 'core/app/type/httpPlugin'
},
[AppTypeEnum.folder]: undefined
});

View File

@@ -1,5 +1,5 @@
import { Box, Flex, TextareaProps } from '@chakra-ui/react';
import React from 'react';
import React, { useTransition } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ChatFunctionTip from './Tip';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
@@ -8,6 +8,7 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const WelcomeTextConfig = (props: TextareaProps) => {
const { t } = useTranslation();
return (
<>
<Flex alignItems={'center'}>
@@ -27,4 +28,4 @@ const WelcomeTextConfig = (props: TextareaProps) => {
);
};
export default WelcomeTextConfig;
export default React.memo(WelcomeTextConfig);

View File

@@ -1,300 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import ReactFlow, {
Background,
Connection,
Controls,
ControlButton,
MiniMap,
NodeProps,
ReactFlowProvider,
useReactFlow,
NodeChange,
OnConnectStartParams,
addEdge,
EdgeChange,
Edge
} from 'reactflow';
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import dynamic from 'next/dynamic';
import ButtonEdge from './components/ButtonEdge';
import NodeTemplatesModal from './NodeTemplatesModal';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useKeyboard } from './hooks/useKeyboard';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.emptyNode]: NodeSimple,
[FlowNodeTypeEnum.globalVariable]: NodeSimple,
[FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')),
[FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')),
[FlowNodeTypeEnum.chatNode]: NodeSimple,
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(() => import('./nodes/NodeDatasetConcat')),
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./nodes/NodeAnswer')),
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./nodes/NodeCQNode')),
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./nodes/NodeHttp')),
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
[FlowNodeTypeEnum.tools]: dynamic(() => import('./nodes/NodeTools')),
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowNodeItemType>) => (
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
),
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
};
const Container = React.memo(function Container() {
const { toast } = useToast();
const { t } = useTranslation();
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const { isDowningCtrl } = useKeyboard();
const {
setConnectingEdge,
reactFlowWrapper,
nodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
setHoverEdgeId
} = useContextSelector(WorkflowContext, (v) => v);
/* node */
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
for (const change of changes) {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
if (node && node.data.forbidDelete) {
return toast({
status: 'warning',
title: t('core.workflow.Can not delete node')
});
} else {
return onOpenConfirmDeleteNode(() => {
onNodesChange(changes);
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
})();
}
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
change.selected = true;
}
}
onNodesChange(changes);
},
[isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast]
);
const handleEdgeChange = useCallback(
(changes: EdgeChange[]) => {
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
},
[onEdgesChange]
);
/* connect */
const onConnectStart = useCallback(
(event: any, params: OnConnectStartParams) => {
setConnectingEdge(params);
},
[setConnectingEdge]
);
const onConnectEnd = useCallback(() => {
setConnectingEdge(undefined);
}, [setConnectingEdge]);
const onConnect = useCallback(
({ connect }: { connect: Connection }) => {
setEdges((state) =>
addEdge(
{
...connect,
type: EDGE_TYPE
},
state
)
);
},
[setEdges]
);
const customOnConnect = useCallback(
(connect: Connection) => {
if (!connect.sourceHandle || !connect.targetHandle) {
return;
}
if (connect.source === connect.target) {
return toast({
status: 'warning',
title: t('core.module.Can not connect self')
});
}
onConnect({
connect
});
},
[onConnect, t, toast]
);
/* edge */
const onEdgeMouseEnter = useCallback(
(e: any, edge: Edge) => {
setHoverEdgeId(edge.id);
},
[setHoverEdgeId]
);
const onEdgeMouseLeave = useCallback(() => {
setHoverEdgeId(undefined);
}, [setHoverEdgeId]);
return (
<>
<ReactFlow
ref={reactFlowWrapper}
fitView
nodes={nodes}
edges={edges}
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={defaultEdgeOptions}
elevateEdgesOnSelect
connectionLineStyle={connectionLineStyle}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgeChange}
onConnect={customOnConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
>
<FlowController />
</ReactFlow>
<ConfirmDeleteModal />
</>
);
});
const Flow = ({ Header, ...data }: { Header: React.ReactNode }) => {
const {
isOpen: isOpenTemplate,
onOpen: onOpenTemplate,
onClose: onCloseTemplate
} = useDisclosure();
const memoRenderContainer = useMemo(() => {
return (
<Box
minH={'400px'}
flex={'1 0 0'}
w={'100%'}
h={0}
position={'relative'}
onContextMenu={(e) => {
e.preventDefault();
return false;
}}
>
{/* open module template */}
<IconButton
position={'absolute'}
top={5}
left={5}
size={'mdSquare'}
borderRadius={'50%'}
icon={<SmallCloseIcon fontSize={'26px'} />}
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
transition={'0.2s ease'}
aria-label={''}
zIndex={1}
boxShadow={'2px 2px 6px #85b1ff'}
onClick={() => {
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
}}
/>
<Container {...data} />
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</Box>
);
}, [data, isOpenTemplate, onCloseTemplate, onOpenTemplate]);
return (
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
<ReactFlowProvider>
<Flex h={'100%'} flexDirection={'column'} bg={'myGray.50'}>
{Header}
{memoRenderContainer}
</Flex>
</ReactFlowProvider>
</Box>
);
};
export default React.memo(Flow);
const FlowController = React.memo(function FlowController() {
const { fitView } = useReactFlow();
return (
<>
<MiniMap
style={{
height: 78,
width: 126,
marginBottom: 35
}}
pannable
/>
<Controls
position={'bottom-right'}
style={{
display: 'flex',
marginBottom: 5,
background: 'white',
borderRadius: '6px',
overflow: 'hidden',
boxShadow:
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
}}
showInteractive={false}
showFitView={false}
>
<MyTooltip label={'页面居中'}>
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
<MyIcon name={'core/modules/fixview'} w={'14px'} />
</ControlButton>
</MyTooltip>
</Controls>
<Background />
</>
);
});

View File

@@ -41,6 +41,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MyBox from '@fastgpt/web/components/common/MyBox';
type EditProps = EditApiKeyProps & { _id?: string };
const defaultEditData: EditProps = {
@@ -84,8 +85,14 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
}, []);
return (
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<Box display={['block', 'flex']} py={[0, 3]} px={5} alignItems={'center'}>
<MyBox
isLoading={isGetting || isDeleting}
display={'flex'}
flexDirection={'column'}
h={'100%'}
position={'relative'}
>
<Box display={['block', 'flex']} alignItems={'center'}>
<Box flex={1}>
<Flex alignItems={'flex-end'}>
<Box color={'myGray.900'} fontSize={'lg'}>
@@ -103,7 +110,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
</Link>
)}
</Flex>
<Box fontSize={'xs'} color={'myGray.600'}>
<Box fontSize={'mini'} color={'myGray.600'}>
{tips}
</Box>
</Box>
@@ -140,7 +147,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
</Button>
</Box>
</Box>
<TableContainer mt={2} position={'relative'} minH={'300px'}>
<TableContainer mt={3} position={'relative'} minH={'300px'}>
<Table>
<Thead>
<Tr>
@@ -225,7 +232,6 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
))}
</Tbody>
</Table>
<Loading loading={isGetting || isDeleting} fixed={false} />
</TableContainer>
{!!editData && (
@@ -279,7 +285,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
</Button>
</ModalFooter>
</MyModal>
</Flex>
</MyBox>
);
};

View File

@@ -40,7 +40,7 @@ const ConfigPerModal = ({
<ModalBody>
<HStack>
<Avatar src={avatar} w={'1.75rem'} />
<Box fontSize={'lg'}>{name}</Box>
<Box>{name}</Box>
</HStack>
<Box mt={6}>
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>

View File

@@ -3,7 +3,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import RowTabs from '../../../../common/Rowtabs';
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
import { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';

View File

@@ -11,7 +11,6 @@ export type AppUpdateParams = {
nodes?: AppSchema['modules'];
edges?: AppSchema['edges'];
chatConfig?: AppSchema['chatConfig'];
permission?: AppSchema['permission'];
teamTags?: AppSchema['teamTags'];
defaultPermission?: AppSchema['defaultPermission'];
};
@@ -28,4 +27,5 @@ export type PostRevertAppProps = {
// edit workflow
editNodes: AppSchema['modules'];
editEdges: AppSchema['edges'];
editChatConfig: AppSchema['chatConfig'];
};

View File

@@ -6,8 +6,7 @@ export type PostWorkflowDebugProps = {
nodes: RuntimeNodeItemType[];
edges: RuntimeEdgeItemType[];
variables: Record<string, any>;
appId?: string;
pluginId?: string;
appId: string;
};
export type PostWorkflowDebugResponse = {

View File

@@ -1,6 +1,6 @@
import type { AppProps } from 'next/app';
import Script from 'next/script';
import Head from 'next/head';
import Layout from '@/components/Layout';
import { appWithTranslation } from 'next-i18next';

View File

@@ -2,10 +2,15 @@ import React from 'react';
import { useTranslation } from 'next-i18next';
import ApiKeyTable from '@/components/support/apikey/Table';
import { useI18n } from '@/web/context/I18n';
import { Box } from '@chakra-ui/react';
const ApiKey = () => {
const { publishT } = useI18n();
return <ApiKeyTable tips={publishT('key tips')}></ApiKeyTable>;
return (
<Box px={[4, 8]} py={[4, 6]}>
<ApiKeyTable tips={publishT('key tips')}></ApiKeyTable>
</Box>
);
};
export default ApiKey;

View File

@@ -0,0 +1,163 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { NextAPI } from '@/service/middleware/entry';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
/*
1. 先读取 HTTP plugin 内容,并找到所有的子plugin,然后事务批量创建,最后修改 inited
2. 读取剩下未 inited 的plugin,逐一创建
*/
let success = 0;
async function handler(req: NextApiRequest, res: NextApiResponse) {
await authCert({ req, authRoot: true });
const total = await MongoPlugin.countDocuments({
inited: { $ne: true }
});
console.log('Total plugin', total);
await initHttp();
await initPlugin();
}
async function initHttp(): Promise<any> {
/* 读取http插件和他的children */
const plugin = await MongoPlugin.findOne({
type: PluginTypeEnum.folder,
inited: { $ne: true }
}).lean();
if (!plugin) return;
const children = await MongoPlugin.find({
teamId: plugin.teamId,
parentId: plugin._id,
inited: { $ne: true }
}).lean();
await mongoSessionRun(async (session) => {
/* 创建HTTP插件作为目录 */
const [{ _id }] = await MongoApp.create(
[
{
teamId: plugin.teamId,
tmbId: plugin.tmbId,
type: AppTypeEnum.httpPlugin,
name: plugin.name,
avatar: plugin.avatar,
intro: plugin.intro,
metadata: plugin.metadata,
version: 'v2',
pluginData: {
apiSchemaStr: plugin.metadata?.apiSchemaStr,
customHeaders: plugin.metadata?.customHeaders
}
}
],
{ session }
);
/* 批量创建子插件 */
for await (const item of children) {
await MongoApp.create(
[
{
parentId: _id,
teamId: item.teamId,
tmbId: item.tmbId,
type: AppTypeEnum.plugin,
name: item.name,
avatar: item.avatar,
intro: item.intro,
version: 'v2',
modules: item.modules,
edges: item.edges,
pluginData: {
nodeVersion: item.nodeVersion,
pluginUniId: plugin.metadata?.pluginUid
}
}
],
{ session }
);
}
/* 更新插件信息 */
await MongoPlugin.findOneAndUpdate(
{
_id: plugin._id
},
{
$set: { inited: true }
},
{ session }
);
await MongoPlugin.updateMany(
{
teamId: plugin.teamId,
parentId: plugin._id
},
{
$set: { inited: true }
},
{ session }
);
success += children.length + 1;
console.log(success);
});
return initHttp();
}
async function initPlugin(): Promise<any> {
const plugin = await MongoPlugin.findOne({
type: PluginTypeEnum.custom,
inited: { $ne: true }
}).lean();
if (!plugin) return;
await mongoSessionRun(async (session) => {
await MongoApp.create(
[
{
teamId: plugin.teamId,
tmbId: plugin.tmbId,
type: AppTypeEnum.plugin,
name: plugin.name,
avatar: plugin.avatar,
intro: plugin.intro,
version: 'v2',
modules: plugin.modules,
edges: plugin.edges,
pluginData: {
nodeVersion: plugin.nodeVersion
}
}
],
{ session }
);
await MongoPlugin.findOneAndUpdate(
{
_id: plugin._id
},
{
$set: { inited: true }
},
{ session }
);
success++;
console.log(success);
});
return initPlugin();
}
export default NextAPI(handler);

View File

@@ -12,6 +12,8 @@ import type { AppSchema } from '@fastgpt/global/core/app/type';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
import { ClientSession } from '@fastgpt/service/common/mongo';
export type CreateAppBody = {
parentId?: ParentIdType;
@@ -35,37 +37,16 @@ async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse
// 上限校验
await checkTeamAppLimit(teamId);
// 创建模型
const appId = await mongoSessionRun(async (session) => {
const [{ _id: appId }] = await MongoApp.create(
[
{
...parseParentIdInMongo(parentId),
avatar,
name,
teamId,
tmbId,
modules,
edges,
type,
version: 'v2'
}
],
{ session }
);
await MongoAppVersion.create(
[
{
appId,
nodes: modules,
edges
}
],
{ session }
);
return appId;
// 创建app
const appId = await onCreateApp({
parentId,
name,
avatar,
type,
modules,
edges,
teamId,
tmbId
});
jsonRes(res, {
@@ -74,3 +55,72 @@ async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse
}
export default NextAPI(handler);
export const onCreateApp = async ({
parentId,
name,
intro,
avatar,
type,
modules,
edges,
teamId,
tmbId,
pluginData,
session
}: {
parentId?: ParentIdType;
name?: string;
avatar?: string;
type?: AppTypeEnum;
modules?: AppSchema['modules'];
edges?: AppSchema['edges'];
intro?: string;
teamId: string;
tmbId: string;
pluginData?: AppSchema['pluginData'];
session?: ClientSession;
}) => {
const create = async (session: ClientSession) => {
const [{ _id: appId }] = await MongoApp.create(
[
{
...parseParentIdInMongo(parentId),
avatar,
name,
intro,
teamId,
tmbId,
modules,
edges,
type,
version: 'v2',
pluginData,
...(type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': defaultNodeVersion })
}
],
{ session }
);
if (type !== AppTypeEnum.folder && type !== AppTypeEnum.httpPlugin) {
await MongoAppVersion.create(
[
{
appId,
nodes: modules,
edges
}
],
{ session }
);
}
return appId;
};
if (session) {
return create(session);
} else {
return await mongoSessionRun(create);
}
};

View File

@@ -14,6 +14,7 @@ import {
} from '@fastgpt/global/support/permission/constant';
import { findAppAndAllChildren } from '@fastgpt/service/core/app/controller';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { ClientSession } from '@fastgpt/service/common/mongo';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
@@ -25,13 +26,30 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
// Auth owner (folder owner, can delete all apps in the folder)
const { teamId } = await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
await onDelOneApp({
teamId,
appId
});
}
export default NextAPI(handler);
export const onDelOneApp = async ({
teamId,
appId,
session
}: {
teamId: string;
appId: string;
session?: ClientSession;
}) => {
const apps = await findAppAndAllChildren({
teamId,
appId,
fields: '_id'
});
await mongoSessionRun(async (session) => {
const del = async (session: ClientSession) => {
for await (const app of apps) {
const appId = app._id;
// Chats
@@ -83,7 +101,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
{ session }
);
}
});
}
};
export default NextAPI(handler);
if (session) {
return del(session);
}
return mongoSessionRun(del);
};

View File

@@ -0,0 +1,68 @@
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/app/httpPlugin/utils';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { onCreateApp, type CreateAppBody } from '../create';
import { AppSchema } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
export type createHttpPluginQuery = {};
export type createHttpPluginBody = Omit<CreateAppBody, 'type' | 'modules' | 'edges'> & {
intro?: string;
pluginData: AppSchema['pluginData'];
};
export type createHttpPluginResponse = {};
async function handler(
req: ApiRequestProps<createHttpPluginBody, createHttpPluginQuery>,
res: ApiResponseType<any>
): Promise<createHttpPluginResponse> {
const { parentId, name, intro, avatar, pluginData } = req.body;
if (!name || !pluginData) {
return Promise.reject('缺少参数');
}
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
await mongoSessionRun(async (session) => {
// create http plugin folder
const httpPluginIid = await onCreateApp({
parentId,
name,
avatar,
intro,
teamId,
tmbId,
type: AppTypeEnum.httpPlugin,
pluginData,
session
});
// compute children plugins
const childrenPlugins = await httpApiSchema2Plugins({
parentId: httpPluginIid,
apiSchemaStr: pluginData.apiSchemaStr,
customHeader: pluginData.customHeaders
});
// create children plugins
for await (const item of childrenPlugins) {
await onCreateApp({
...item,
teamId,
tmbId,
session
});
}
});
return {};
}
export default NextAPI(handler);

View File

@@ -0,0 +1,127 @@
import type { NextApiResponse } from 'next';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { ClientSession } from '@fastgpt/service/common/mongo';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/app/httpPlugin/utils';
import { NextAPI } from '@/service/middleware/entry';
import { AppSchema } from '@fastgpt/global/core/app/type';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { isEqual } from 'lodash';
import { onCreateApp } from '../create';
import { onDelOneApp } from '../del';
export type UpdateHttpPluginBody = {
appId: string;
name: string;
avatar?: string;
intro?: string;
pluginData: AppSchema['pluginData'];
};
async function handler(req: ApiRequestProps<UpdateHttpPluginBody>, res: NextApiResponse<any>) {
const { appId, name, avatar, intro, pluginData } = req.body;
const { app } = await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
const storeData = {
apiSchemaStr: app.pluginData?.apiSchemaStr,
customHeaders: app.pluginData?.customHeaders
};
const updateData = {
apiSchemaStr: pluginData?.apiSchemaStr,
customHeaders: pluginData?.customHeaders
};
await mongoSessionRun(async (session) => {
// update children
if (!isEqual(storeData, updateData)) {
await updateHttpChildrenPlugin({
teamId: app.teamId,
tmbId: app.tmbId,
parentId: app._id,
pluginData,
session
});
}
await MongoApp.findByIdAndUpdate(
appId,
{
name,
avatar,
intro,
pluginData
},
{ session }
);
});
}
export default NextAPI(handler);
const updateHttpChildrenPlugin = async ({
teamId,
tmbId,
parentId,
pluginData,
session
}: {
teamId: string;
tmbId: string;
parentId: string;
pluginData?: AppSchema['pluginData'];
session: ClientSession;
}) => {
if (!pluginData?.apiSchemaStr) return;
const dbPlugins = await MongoApp.find({
parentId,
teamId
}).select({
pluginData: 1
});
const schemaPlugins = await httpApiSchema2Plugins({
parentId,
apiSchemaStr: pluginData?.apiSchemaStr,
customHeader: pluginData?.customHeaders
});
// 数据库中存在schema不存在删除
for await (const plugin of dbPlugins) {
if (!schemaPlugins.find((p) => p.name === plugin.pluginData?.pluginUniId)) {
await onDelOneApp({
teamId,
appId: plugin._id,
session
});
}
}
// 数据库中不存在schema存在新增
for await (const plugin of schemaPlugins) {
if (!dbPlugins.find((p) => p.pluginData?.pluginUniId === plugin.name)) {
await onCreateApp({
...plugin,
teamId,
tmbId,
session
});
}
}
// 数据库中存在schema存在更新
for await (const plugin of schemaPlugins) {
const dbPlugin = dbPlugins.find((p) => plugin.name === p.pluginData?.pluginUniId);
if (dbPlugin) {
await MongoApp.findByIdAndUpdate(
dbPlugin._id,
{
...plugin,
version: 'v2'
},
{ session }
);
}
}
};

View File

@@ -17,8 +17,9 @@ import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/
export type ListAppBody = {
parentId?: ParentIdType;
type?: AppTypeEnum;
type?: AppTypeEnum | AppTypeEnum[];
getRecentlyChat?: boolean;
searchKey?: string;
};
async function handler(
@@ -36,26 +37,43 @@ async function handler(
per: ReadPermissionVal
});
const { parentId, type, getRecentlyChat } = req.body;
const { parentId, type, getRecentlyChat, searchKey } = req.body;
const findAppsQuery = getRecentlyChat
? {
const findAppsQuery = (() => {
const searchMatch = searchKey
? {
$or: [
{ name: { $regex: searchKey, $options: 'i' } },
{ intro: { $regex: searchKey, $options: 'i' } }
]
}
: {};
if (getRecentlyChat) {
return {
// get all chat app
teamId,
type: { $in: [AppTypeEnum.advanced, AppTypeEnum.simple] }
}
: {
teamId,
...(type && { type }),
...parseParentIdInMongo(parentId)
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple] },
...searchMatch
};
}
return {
teamId,
...(type && Array.isArray(type) && { type: { $in: type } }),
...(type && { type }),
...parseParentIdInMongo(parentId),
...searchMatch
};
})();
/* temp: get all apps and per */
const [myApps, rpList] = await Promise.all([
MongoApp.find(findAppsQuery, '_id avatar type name intro tmbId defaultPermission')
MongoApp.find(findAppsQuery, '_id avatar type name intro tmbId pluginData defaultPermission')
.sort({
updateTime: -1
})
.limit(searchKey ? 20 : 1000)
.lean(),
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app,
@@ -88,7 +106,8 @@ async function handler(
name: app.name,
intro: app.intro,
permission: app.permission,
defaultPermission: app.defaultPermission || AppDefaultPermissionVal
defaultPermission: app.defaultPermission || AppDefaultPermissionVal,
pluginData: app.pluginData
}));
}

View File

@@ -0,0 +1,33 @@
/*
get plugin preview modules
*/
import type { NextApiResponse } from 'next';
import {
getPluginPreviewNode,
splitCombinePluginId
} from '@fastgpt/service/core/app/plugin/controller';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
export type GetPreviewNodeQuery = { appId: string };
async function handler(
req: ApiRequestProps<{}, GetPreviewNodeQuery>,
res: NextApiResponse<any>
): Promise<FlowNodeTemplateType> {
const { appId } = req.query;
const { source } = await splitCombinePluginId(appId);
if (source === PluginSourceEnum.personal) {
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
}
return getPluginPreviewNode({ id: appId });
}
export default NextAPI(handler);

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTypeEnum, defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
@@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
intro: plugin.intro,
showStatus: true,
isTool: plugin.isTool,
version: '481',
version: defaultNodeVersion,
inputs: [],
outputs: []
})) || [];

View File

@@ -0,0 +1,53 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { onCreateApp } from './create';
export type transitionWorkflowQuery = {};
export type transitionWorkflowBody = {
appId: string;
createNew?: boolean;
};
export type transitionWorkflowResponse = {
id?: string;
};
async function handler(
req: ApiRequestProps<transitionWorkflowBody, transitionWorkflowQuery>,
res: ApiResponseType<any>
): Promise<transitionWorkflowResponse> {
const { appId, createNew } = req.body;
const { app, tmbId } = await authApp({
req,
appId,
authToken: true,
per: OwnerPermissionVal
});
if (createNew) {
const appId = await onCreateApp({
parentId: app.parentId,
name: app.name + ' Copy',
avatar: app.avatar,
type: AppTypeEnum.workflow,
modules: app.modules,
edges: app.edges,
teamId: app.teamId,
tmbId
});
return { id: appId };
} else {
await MongoApp.findByIdAndUpdate(appId, { type: AppTypeEnum.workflow });
}
return {};
}
export default NextAPI(handler);

View File

@@ -6,8 +6,7 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middleware/entry';
import {
ManagePermissionVal,
WritePermissionVal,
OwnerPermissionVal
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
@@ -22,7 +21,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
nodes,
edges,
chatConfig,
permission,
teamTags,
defaultPermission
} = req.body as AppUpdateParams;
@@ -33,9 +31,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}
// 凭证校验
if (permission) {
await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
} else if (defaultPermission) {
if (defaultPermission) {
await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
} else {
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
@@ -56,7 +52,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
type,
avatar,
intro,
permission,
defaultPermission,
...(teamTags && teamTags),
...(formatNodes && {

View File

@@ -0,0 +1,36 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
export type getLatestVersionQuery = {
appId: string;
};
export type getLatestVersionBody = {};
export type getLatestVersionResponse = {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
chatConfig: AppChatConfigType;
};
async function handler(
req: ApiRequestProps<getLatestVersionBody, getLatestVersionQuery>,
res: ApiResponseType<any>
): Promise<getLatestVersionResponse> {
const { app } = await authApp({
req,
authToken: true,
appId: req.query.appId,
per: WritePermissionVal
});
return getAppLatestVersion(req.query.appId, app);
}
export default NextAPI(handler);

View File

@@ -8,6 +8,7 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostPublishAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
type Response = {};
@@ -15,13 +16,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
const { appId } = req.query as { appId: string };
const { nodes = [], edges = [], chatConfig, type } = req.body as PostPublishAppProps;
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { app } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
await mongoSessionRun(async (session) => {
// create version histories
await MongoAppVersion.create(
const [{ _id }] = await MongoAppVersion.create(
[
{
appId,
@@ -34,18 +35,25 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
);
// update app
await MongoApp.findByIdAndUpdate(appId, {
modules: formatNodes,
edges,
chatConfig,
updateTime: new Date(),
version: 'v2',
type,
scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig
? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
: null
});
await MongoApp.findByIdAndUpdate(
appId,
{
modules: formatNodes,
edges,
chatConfig,
updateTime: new Date(),
version: 'v2',
type,
scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig?.cronString
? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
: null,
...(app.type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': _id })
},
{
session
}
);
});
return {};

View File

@@ -8,14 +8,20 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostRevertAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
type Response = {};
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> {
const { appId } = req.query as { appId: string };
const { editNodes = [], editEdges = [], versionId } = req.body as PostRevertAppProps;
const {
editNodes = [],
editEdges = [],
editChatConfig,
versionId
} = req.body as PostRevertAppProps;
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { app } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const version = await MongoAppVersion.findOne({
_id: versionId,
@@ -37,19 +43,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
{
appId,
nodes: formatEditNodes,
edges: editEdges
edges: editEdges,
chatConfig: editChatConfig
}
],
{ session }
);
// 为历史版本再创建一个版本
await MongoAppVersion.create(
const [{ _id }] = await MongoAppVersion.create(
[
{
appId,
nodes: version.nodes,
edges: version.edges
edges: version.edges,
chatConfig: version.chatConfig
}
],
{ session }
@@ -59,11 +67,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
await MongoApp.findByIdAndUpdate(appId, {
modules: version.nodes,
edges: version.edges,
chatConfig: version.chatConfig,
updateTime: new Date(),
scheduledTriggerConfig,
scheduledTriggerNextTime: scheduledTriggerConfig
scheduledTriggerNextTime: scheduledTriggerConfig?.cronString
? getNextTimeByCronStringAndTimezone(scheduledTriggerConfig)
: null
: null,
...(app.type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': _id })
});
});

View File

@@ -6,6 +6,7 @@ import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
export type QueryChatInputGuideBody = OutLinkChatAuthProps & {
appId: string;
@@ -28,7 +29,7 @@ async function handler(
const params = {
appId,
...(searchKey && { text: { $regex: new RegExp(searchKey, 'i') } })
...(searchKey && { text: { $regex: new RegExp(`${replaceRegChars(searchKey)}`, 'i') } })
};
const result = await MongoChatInputGuide.find(params).sort({ _id: -1 }).limit(6);

View File

@@ -1,78 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/plugin/httpPlugin/utils';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const body = req.body as CreateOnePluginParams;
// await checkTeamPluginLimit(teamId);
// create parent plugin and child plugin
if (body.metadata?.apiSchemaStr) {
const parentId = await mongoSessionRun(async (session) => {
const [{ _id: parentId }] = await MongoPlugin.create(
[
{
...body,
parentId: null,
teamId,
tmbId,
version: 'v2'
}
],
{ session }
);
const childrenPlugins = await httpApiSchema2Plugins({
parentId,
apiSchemaStr: body.metadata?.apiSchemaStr,
customHeader: body.metadata?.customHeaders
});
await MongoPlugin.create(
childrenPlugins.map((item) => ({
...item,
metadata: {
pluginUid: item.name
},
teamId,
tmbId,
version: 'v2'
})),
{
session
}
);
return parentId;
});
jsonRes(res, {
data: parentId
});
} else {
const { _id } = await MongoPlugin.create({
...body,
teamId,
tmbId,
version: 'v2'
});
jsonRes(res, {
data: _id
});
}
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,41 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { teamId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const { pluginId } = req.query as { pluginId: string };
if (!pluginId) {
throw new Error('缺少参数');
}
await authPluginCrud({ req, authToken: true, pluginId, per: 'owner' });
await mongoSessionRun(async (session) => {
await MongoPlugin.deleteMany(
{
teamId,
parentId: pluginId
},
{
session
}
);
await MongoPlugin.findByIdAndDelete(pluginId, { session });
});
jsonRes(res, {});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,21 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { plugin } = await authPluginCrud({ req, authToken: true, pluginId: id, per: 'r' });
jsonRes(res, {
data: plugin
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,28 +0,0 @@
/*
get plugin preview modules
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { getPluginPreviewNode } from '@fastgpt/service/core/plugin/controller';
import { authPluginCanUse } from '@fastgpt/service/support/permission/auth/plugin';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as { id: string };
await connectToDatabase();
const { teamId, tmbId } = await authCert({ req, authToken: true });
await authPluginCanUse({ id, teamId, tmbId });
jsonRes<FlowNodeTemplateType>(res, {
data: await getPluginPreviewNode({ id })
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,36 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { PluginListItemType } from '@fastgpt/global/core/plugin/controller';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { parentId, type } = req.query as { parentId?: string; type?: DatasetTypeEnum };
const { teamId } = await authCert({ req, authToken: true });
const plugins = await MongoPlugin.find(
{
teamId,
...(parentId !== undefined && { parentId: parentId || null }),
...(type && { type })
},
'_id parentId type name avatar intro metadata'
)
.sort({ updateTime: -1 })
.lean();
jsonRes<PluginListItemType[]>(res, {
data: plugins
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,46 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type.d';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { parentId } = req.query as { parentId: string };
if (!parentId) {
return jsonRes(res, {
data: []
});
}
await authCert({ req, authToken: true });
jsonRes<ParentTreePathItemType[]>(res, {
data: await getParents(parentId)
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
async function getParents(parentId?: string): Promise<ParentTreePathItemType[]> {
if (!parentId) {
return [];
}
const parent = await MongoPlugin.findById(parentId, 'name parentId');
if (!parent) return [];
const paths = await getParents(parent.parentId);
paths.push({ parentId, parentName: parent.name });
return paths;
}

View File

@@ -1,64 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { parentId, searchKey } = req.query as { parentId?: string; searchKey?: string };
const { teamId } = await authCert({ req, authToken: true });
const userPlugins = await (async () => {
if (searchKey) {
return MongoPlugin.find({
teamId,
// search name or intro
$or: [
{ name: { $regex: searchKey, $options: 'i' } },
{ intro: { $regex: searchKey, $options: 'i' } }
]
})
.sort({
updateTime: -1
})
.lean();
} else {
return MongoPlugin.find({ teamId, parentId: parentId || null })
.sort({
updateTime: -1
})
.lean();
}
})();
const data: FlowNodeTemplateType[] = userPlugins.map((plugin) => ({
id: String(plugin._id),
parentId: String(plugin.parentId),
pluginId: String(plugin._id),
pluginType: plugin.type,
templateType: FlowNodeTemplateTypeEnum.personalPlugin,
flowNodeType: FlowNodeTypeEnum.pluginModule,
avatar: plugin.avatar,
name: plugin.name,
intro: plugin.intro,
showStatus: false,
version: '481',
inputs: [],
outputs: []
}));
jsonRes<FlowNodeTemplateType[]>(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,152 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { UpdatePluginParams } from '@fastgpt/global/core/plugin/controller';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { ClientSession } from '@fastgpt/service/common/mongo';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/plugin/httpPlugin/utils';
import { isEqual } from 'lodash';
import { nanoid } from 'nanoid';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const body = req.body as UpdatePluginParams;
const { id, modules, edges, ...props } = body;
const { teamId, tmbId } = await authPluginCrud({
req,
authToken: true,
pluginId: id,
per: 'owner'
});
const originPlugin = await MongoPlugin.findById(id);
let updateData = {
name: props.name,
intro: props.intro,
avatar: props.avatar,
parentId: props.parentId,
version: 'v2',
...(modules?.length && {
modules: modules
}),
...(edges?.length && { edges }),
metadata: props.metadata,
nodeVersion: originPlugin?.nodeVersion
};
const isNodeVersionEqual =
isEqual(
originPlugin?.modules.map((module) => {
return { ...module, position: undefined };
}),
updateData.modules?.map((module) => {
return { ...module, position: undefined };
})
) && isEqual(originPlugin?.edges, updateData.edges);
if (!isNodeVersionEqual) {
updateData = {
...updateData,
nodeVersion: nanoid(6)
};
}
if (props.metadata?.apiSchemaStr) {
await mongoSessionRun(async (session) => {
// update children
await updateHttpChildrenPlugin({
teamId,
tmbId,
parent: body,
session
});
await MongoPlugin.findByIdAndUpdate(id, updateData, { session });
});
jsonRes(res, {});
} else {
jsonRes(res, {
data: await MongoPlugin.findByIdAndUpdate(id, updateData)
});
}
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
const updateHttpChildrenPlugin = async ({
teamId,
tmbId,
parent,
session
}: {
teamId: string;
tmbId: string;
parent: UpdatePluginParams;
session: ClientSession;
}) => {
if (!parent.metadata?.apiSchemaStr) return;
const dbPlugins = await MongoPlugin.find(
{
parentId: parent.id,
teamId
},
'_id metadata'
);
const schemaPlugins = await httpApiSchema2Plugins({
parentId: parent.id,
apiSchemaStr: parent.metadata?.apiSchemaStr,
customHeader: parent.metadata?.customHeaders
});
// 数据库中存在schema不存在删除
for await (const plugin of dbPlugins) {
if (!schemaPlugins.find((p) => p.name === plugin.metadata?.pluginUid)) {
await MongoPlugin.deleteOne({ _id: plugin._id }, { session });
}
}
// 数据库中不存在schema存在新增
for await (const plugin of schemaPlugins) {
if (!dbPlugins.find((p) => p.metadata?.pluginUid === plugin.name)) {
await MongoPlugin.create(
[
{
...plugin,
metadata: {
pluginUid: plugin.name
},
teamId,
tmbId,
version: 'v2'
}
],
{
session
}
);
}
}
// 数据库中存在schema存在更新
for await (const plugin of schemaPlugins) {
const dbPlugin = dbPlugins.find((p) => plugin.name === p.metadata?.pluginUid);
if (dbPlugin) {
await MongoPlugin.findByIdAndUpdate(
dbPlugin._id,
{
...plugin,
version: 'v2'
},
{ session }
);
}
}
};

View File

@@ -6,7 +6,6 @@ import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { defaultApp } from '@/web/core/app/constants';
@@ -15,13 +14,7 @@ async function handler(
req: NextApiRequest,
res: NextApiResponse
): Promise<PostWorkflowDebugResponse> {
const {
nodes = [],
edges = [],
variables = {},
appId,
pluginId
} = req.body as PostWorkflowDebugProps;
const { nodes = [], edges = [], variables = {}, appId } = req.body as PostWorkflowDebugProps;
if (!nodes) {
throw new Error('Prams Error');
@@ -39,8 +32,7 @@ async function handler(
req,
authToken: true
}),
appId && authApp({ req, authToken: true, appId, per: ReadPermissionVal }),
pluginId && authPluginCrud({ req, authToken: true, pluginId, per: 'r' })
authApp({ req, authToken: true, appId, per: ReadPermissionVal })
]);
// auth balance

View File

@@ -1,375 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { useTranslation } from 'next-i18next';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ChatTest, { type ChatTestComponentRef } from '@/components/core/workflow/Flow/ChatTest';
import { uiWorkflow2StoreWorkflow } from '@/components/core/workflow/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { getErrText } from '@fastgpt/global/common/error/utils';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
checkWorkflowNodeAndConnection,
filterSensitiveNodesData
} from '@/web/core/workflow/utils';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { formatTime2HM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context';
import { useInterval, useUpdateEffect } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { AppContext } from '@/web/core/app/context/appContext';
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
const PublishHistories = dynamic(
() => import('@/components/core/workflow/components/PublishHistoriesSlider')
);
type Props = { onClose: () => void };
const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
ChatTestRef,
setWorkflowTestData,
onClose
}: Props & {
ChatTestRef: React.RefObject<ChatTestComponentRef>;
setWorkflowTestData: React.Dispatch<
React.SetStateAction<
| {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}
| undefined
>
>;
}) {
const { appDetail } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const theme = useTheme();
const { toast } = useToast();
const { t } = useTranslation();
const { appT } = useI18n();
const { copyData } = useCopyData();
const { openConfirm: openConfigPublish, ConfirmModal } = useConfirm({
content: t('core.app.Publish Confirm')
});
const { publishApp, updateAppDetail } = useContextSelector(AppContext, (v) => v);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const [isSaving, setIsSaving] = useState(false);
const [saveLabel, setSaveLabel] = useState(t('core.app.Onclick to save'));
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const isShowVersionHistories = useContextSelector(
WorkflowContext,
(v) => v.isShowVersionHistories
);
const setIsShowVersionHistories = useContextSelector(
WorkflowContext,
(v) => v.setIsShowVersionHistories
);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const flowData2StoreDataAndCheck = useCallback(async () => {
const { nodes } = await getWorkflowStore();
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
if (!checkResults) {
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
return storeNodes;
} else {
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
toast({
status: 'warning',
title: t('core.workflow.Check Failed')
});
}
}, [edges, onUpdateNodeError, t, toast]);
const onclickSave = useCallback(
async (forbid?: boolean) => {
// version preview / debug mode, not save
if (!isV2Workflow || isShowVersionHistories || forbid) return;
const { nodes } = await getWorkflowStore();
if (nodes.length === 0) return null;
setIsSaving(true);
const storeWorkflow = uiWorkflow2StoreWorkflow({ nodes, edges });
try {
await updateAppDetail({
...storeWorkflow,
type: AppTypeEnum.advanced,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
setSaveLabel(
t('core.app.Auto Save time', {
time: formatTime2HM()
})
);
// ChatTestRef.current?.resetChatTest();
} catch (error) {}
setIsSaving(false);
return null;
},
[isV2Workflow, isShowVersionHistories, edges, updateAppDetail, appDetail.chatConfig, t]
);
const onclickPublish = useCallback(async () => {
setIsSaving(true);
const data = await flowData2StoreDataAndCheck();
if (data) {
try {
await publishApp({
...data,
type: AppTypeEnum.advanced,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
toast({
status: 'success',
title: t('core.app.Publish Success')
});
ChatTestRef.current?.resetChatTest();
} catch (error) {
toast({
status: 'warning',
title: getErrText(error, t('core.app.Publish Failed'))
});
}
}
setIsSaving(false);
}, [flowData2StoreDataAndCheck, publishApp, appDetail.chatConfig, toast, t, ChatTestRef]);
const saveAndBack = useCallback(async () => {
try {
await onclickSave();
onClose();
} catch (error) {}
}, [onClose, onclickSave]);
const onExportWorkflow = useCallback(async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
chatConfig: appDetail.chatConfig
},
null,
2
),
appT('Export Config Successful')
);
}
}, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
// effect
useBeforeunload({
callback: onclickSave,
tip: t('core.common.tip.leave page')
});
useInterval(() => {
if (!appDetail._id) return;
onclickSave(!!workflowDebugData);
}, 20000);
const Render = useMemo(() => {
return (
<>
<Flex
py={3}
px={[2, 5, 8]}
borderBottom={theme.borders.base}
alignItems={'center'}
userSelect={'none'}
bg={'myGray.25'}
h={'67px'}
>
<IconButton
size={'smSquare'}
icon={<MyIcon name={'common/backFill'} w={'14px'} />}
borderRadius={'50%'}
w={'26px'}
h={'26px'}
borderColor={'myGray.300'}
variant={'whiteBase'}
aria-label={''}
isLoading={isSaving}
onClick={saveAndBack}
/>
<Box ml={[2, 4]}>
<Box fontSize={'md'} fontWeight={'bold'}>
{appDetail.name}
</Box>
{!isShowVersionHistories && isV2Workflow && (
<MyTooltip label={t('core.app.Onclick to save')}>
<Box
fontSize={'xs'}
mt={1}
display={'inline-block'}
borderRadius={'xs'}
cursor={'pointer'}
onClick={() => onclickSave()}
color={'myGray.500'}
>
{saveLabel}
</Box>
</MyTooltip>
)}
</Box>
<Box flex={1} />
{!isShowVersionHistories && (
<>
<MyMenu
Button={
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'more'} w={'14px'} p={2} />}
aria-label={''}
size={'sm'}
variant={'whitePrimary'}
/>
}
menuList={[
{
children: [
{
label: appT('Import Configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: appT('Export Configs'),
icon: 'export',
onClick: onExportWorkflow
}
]
}
]}
/>
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => setIsShowVersionHistories(true)}
/>
</>
)}
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
const data = await flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('core.workflow.Debug')}
</Button>
{!isShowVersionHistories && (
<Button
ml={[2, 4]}
size={'sm'}
isLoading={isSaving}
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
onClick={openConfigPublish(onclickPublish)}
>
{t('core.app.Publish')}
</Button>
)}
</Flex>
<ConfirmModal confirmText={t('core.app.Publish')} />
</>
);
}, [
theme.borders.base,
isSaving,
saveAndBack,
appDetail.name,
isShowVersionHistories,
isV2Workflow,
t,
saveLabel,
appT,
onOpenImport,
onExportWorkflow,
openConfigPublish,
onclickPublish,
ConfirmModal,
onclickSave,
setIsShowVersionHistories,
flowData2StoreDataAndCheck,
setWorkflowTestData
]);
return (
<>
{Render}
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
{isShowVersionHistories && <PublishHistories />}
</>
);
});
const Header = (props: Props) => {
const ChatTestRef = useRef<ChatTestComponentRef>(null);
const [workflowTestData, setWorkflowTestData] = useState<{
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}>();
const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure();
useUpdateEffect(() => {
onOpenTest();
}, [workflowTestData]);
return (
<>
<RenderHeaderContainer
{...props}
ChatTestRef={ChatTestRef}
setWorkflowTestData={setWorkflowTestData}
/>
<ChatTest ref={ChatTestRef} isOpen={isOpenTest} {...workflowTestData} onClose={onCloseTest} />
</>
);
};
export default React.memo(Header);

View File

@@ -27,7 +27,7 @@ import {
getCollaboratorList
} from '@/web/core/app/api/collaborator';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
import {
AppDefaultPermissionVal,
AppPermissionList

View File

@@ -11,7 +11,8 @@ import {
Tbody,
useTheme,
useDisclosure,
ModalBody
ModalBody,
HStack
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
@@ -35,13 +36,17 @@ import { formatChatValue2InputType } from '@/components/ChatBox/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useI18n } from '@/web/context/I18n';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { cardStyles } from '../constants';
const Logs = ({ appId }: { appId: string }) => {
const Logs = () => {
const { t } = useTranslation();
const { appT } = useI18n();
const { isPc } = useSystemStore();
const appId = useContextSelector(AppContext, (v) => v.appId);
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
@@ -72,8 +77,8 @@ const Logs = ({ appId }: { appId: string }) => {
const [detailLogsId, setDetailLogsId] = useState<string>();
return (
<Flex flexDirection={'column'} h={'100%'} pt={[1, 5]} position={'relative'}>
<Box px={[4, 8]}>
<>
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
{isPc && (
<>
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mb={2}>
@@ -96,97 +101,106 @@ const Logs = ({ appId }: { appId: string }) => {
</Box>
{/* table */}
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'} px={[4, 8]}>
<Table variant={'simple'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('core.app.logs.Source And Time')}</Th>
<Th>{appT('Logs Title')}</Th>
<Th>{appT('Logs Message Total')}</Th>
<Th>{appT('Feedback Count')}</Th>
<Th>{t('core.app.feedback.Custom feedback')}</Th>
<Th>{appT('Mark Count')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'xs'}>
{logs.map((item) => (
<Tr
key={item._id}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
title={'点击查看对话详情'}
onClick={() => setDetailLogsId(item.id)}
>
<Td>
<Box>{t(ChatSourceMap[item.source]?.name || 'UnKnow')}</Box>
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
</Td>
<Td className="textEllipsis" maxW={'250px'}>
{item.title}
</Td>
<Td>{item.messageCount}</Td>
<Td w={'100px'}>
{!!item?.userGoodFeedbackCount && (
<Flex
mb={item?.userGoodFeedbackCount ? 1 : 0}
bg={'green.100'}
color={'green.600'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/goodLight'}
color={'green.600'}
w={'14px'}
/>
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex
bg={'#FFF2EC'}
color={'#C96330'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/badLight'}
color={'#C96330'}
w={'14px'}
/>
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
<Td>{item.customFeedbacksCount || '-'}</Td>
<Td>{item.markCount}</Td>
<Flex
flexDirection={'column'}
{...cardStyles}
boxShadow={3.5}
mt={4}
px={[4, 8]}
py={[4, 6]}
flex={'1 0 0'}
>
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'}>
<Table variant={'simple'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('core.app.logs.Source And Time')}</Th>
<Th>{appT('Logs Title')}</Th>
<Th>{appT('Logs Message Total')}</Th>
<Th>{appT('Feedback Count')}</Th>
<Th>{t('core.app.feedback.Custom feedback')}</Th>
<Th>{appT('Mark Count')}</Th>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{logs.length === 0 && !isLoading && <EmptyTip text={appT('Logs Empty')}></EmptyTip>}
<Flex w={'100%'} p={4} alignItems={'center'} justifyContent={'flex-end'}>
<DateRangePicker
defaultDate={dateRange}
position="top"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Box ml={3}>
</Thead>
<Tbody fontSize={'xs'}>
{logs.map((item) => (
<Tr
key={item._id}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
title={'点击查看对话详情'}
onClick={() => setDetailLogsId(item.id)}
>
<Td>
<Box>{t(ChatSourceMap[item.source]?.name || 'UnKnow')}</Box>
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
</Td>
<Td className="textEllipsis" maxW={'250px'}>
{item.title}
</Td>
<Td>{item.messageCount}</Td>
<Td w={'100px'}>
{!!item?.userGoodFeedbackCount && (
<Flex
mb={item?.userGoodFeedbackCount ? 1 : 0}
bg={'green.100'}
color={'green.600'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/goodLight'}
color={'green.600'}
w={'14px'}
/>
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex
bg={'#FFF2EC'}
color={'#C96330'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/badLight'}
color={'#C96330'}
w={'14px'}
/>
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
<Td>{item.customFeedbacksCount || '-'}</Td>
<Td>{item.markCount}</Td>
</Tr>
))}
</Tbody>
</Table>
{logs.length === 0 && !isLoading && <EmptyTip text={appT('Logs Empty')}></EmptyTip>}
</TableContainer>
<HStack w={'100%'} mt={3} justifyContent={'flex-end'}>
<DateRangePicker
defaultDate={dateRange}
position="top"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Pagination />
</Box>
</HStack>
</Flex>
{!!detailLogsId && (
@@ -206,7 +220,7 @@ const Logs = ({ appId }: { appId: string }) => {
>
<ModalBody whiteSpace={'pre-wrap'}>{t('core.chat.Mark Description')}</ModalBody>
</MyModal>
</Flex>
</>
);
};

View File

@@ -0,0 +1,191 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
import { useInterval } from 'ahooks';
import { AppContext, TabEnum } from '../context';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => {
const { t } = useTranslation();
const router = useRouter();
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const {
flowData2StoreDataAndCheck,
onSaveWorkflow,
setHistoriesDefaultData,
historiesDefaultData,
initData
} = useContextSelector(WorkflowContext, (v) => v);
const onclickPublish = useCallback(async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
await onPublish({
...data,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
}
}, [flowData2StoreDataAndCheck, onPublish, appDetail.chatConfig]);
const saveAndBack = useCallback(async () => {
try {
await onSaveWorkflow();
router.push('/app/list');
} catch (error) {}
}, [onSaveWorkflow, router]);
// effect
useBeforeunload({
callback: onSaveWorkflow,
tip: t('core.common.tip.leave page')
});
useInterval(() => {
if (!appDetail._id) return;
onSaveWorkflow();
}, 40000);
const Render = useMemo(() => {
return (
<>
{/* {!isPc && (
<Flex pt={2} justifyContent={'center'}>
<RouteTab />
</Flex>
)} */}
<Flex
mt={[2, 0]}
py={3}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
alignItems={'center'}
userSelect={'none'}
h={'67px'}
{...(currentTab === TabEnum.appEdit
? {
bg: 'myGray.25'
}
: {
bg: 'transparent',
borderBottomColor: 'transparent'
})}
>
{/* back */}
<MyIcon
name={'common/leftArrowLight'}
w={'1.75rem'}
cursor={'pointer'}
onClick={saveAndBack}
/>
{/* app info */}
<Box ml={1}>
<AppCard
showSaveStatus={
!historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit
}
/>
</Box>
{/* {isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)} */}
<Box flex={1} />
{currentTab === TabEnum.appEdit && (
<>
{!historiesDefaultData && (
<IconButton
// mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={async () => {
const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore());
setHistoriesDefaultData({
nodes,
edges,
chatConfig: appDetail.chatConfig
});
}}
/>
)}
{/* <Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('core.workflow.Debug')}
</Button> */}
{!historiesDefaultData && (
<PopoverConfirm
showCancel
content={t('core.app.Publish Confirm')}
Trigger={
<Button
ml={[2, 4]}
size={'sm'}
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
>
{t('core.app.Publish')}
</Button>
}
onConfirm={() => onclickPublish()}
/>
)}
</>
)}
</Flex>
{historiesDefaultData && (
<PublishHistories
initData={initData}
onClose={() => {
setHistoriesDefaultData(undefined);
}}
defaultData={historiesDefaultData}
/>
)}
</>
);
}, [
appDetail.chatConfig,
currentTab,
historiesDefaultData,
initData,
isV2Workflow,
onclickPublish,
saveAndBack,
setHistoriesDefaultData,
t
]);
return Render;
};
export default React.memo(Header);

View File

@@ -0,0 +1,72 @@
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 { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
import Header from './Header';
import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import Flow from '../WorkflowComponents/Flow';
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 { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
content:
'检测到您的高级编排为旧版,系统将为您自动格式化成新版工作流。\n\n由于版本差异较大会导致一些工作流无法正常排布请重新手动连接工作流。如仍异常可尝试删除对应节点后重新添加。\n\n你可以直接点击调试进行工作流测试调试完毕后点击发布。直到你点击发布新工作流才会真正保存生效。\n\n在你发布新工作流前自动保存不会生效。'
});
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
useMount(() => {
if (!isV2Workflow) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
})();
} else {
initData(
cloneDeep({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
})
);
}
});
return (
<Flex {...workflowBoxStyles}>
<Header />
{currentTab === TabEnum.appEdit ? (
<Flow />
) : (
<Flex flexDirection={'column'} h={'100%'} px={4} pb={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
)}
{!isV2Workflow && <ConfirmModal countDown={0} />}
</Flex>
);
};
const Render = () => {
return (
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
);
};
export default React.memo(Render);

View File

@@ -6,11 +6,7 @@ import { useI18n } from '@/web/context/I18n';
const API = ({ appId }: { appId: string }) => {
const { publishT } = useI18n();
return (
<Box pt={3}>
<ApiKeyTable tips={publishT('app key tips')} appId={appId} />
</Box>
);
return <ApiKeyTable tips={publishT('app key tips')} appId={appId} />;
};
export default API;

View File

@@ -47,6 +47,7 @@ import { useI18n } from '@/web/context/I18n';
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';
const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal'));
@@ -72,7 +73,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<MyBox h={'100%'} isLoading={isFetching} position={'relative'}>
<Flex justifyContent={'space-between'}>
<HStack>
<Box color={'myGray.900'} fontSize={'lg'}>
@@ -241,8 +242,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
/>
)}
<ConfirmModal />
<Loading loading={isFetching} fixed={false} />
</Box>
</MyBox>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useRef, useState } from 'react';
import { Box, useTheme } from '@chakra-ui/react';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import dynamic from 'next/dynamic';
@@ -7,13 +7,20 @@ import dynamic from 'next/dynamic';
import MyRadio from '@/components/common/MyRadio';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { cardStyles } from '../constants';
import Link from './Link';
const API = dynamic(() => import('./API'));
const FeiShu = dynamic(() => import('./FeiShu'));
const OutLink = ({ appId }: { appId: string }) => {
const OutLink = () => {
const { t } = useTranslation();
const theme = useTheme();
const appId = useContextSelector(AppContext, (v) => v.appId);
const publishList = useRef([
{
icon: '/imgs/modal/shareFill.svg',
@@ -38,11 +45,8 @@ const OutLink = ({ appId }: { appId: string }) => {
const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share);
return (
<Box pt={[1, 5]}>
<Box color={'myGray.900'} fontSize={'lg'} mb={2} px={[4, 8]}>
{t('core.app.navbar.Publish app')}
</Box>
<Box pb={[5, 7]} px={[4, 8]} borderBottom={theme.borders.base}>
<>
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
<MyRadio
gridTemplateColumns={['repeat(1,1fr)', 'repeat(auto-fill, minmax(0, 400px))']}
iconSize={'20px'}
@@ -52,12 +56,22 @@ const OutLink = ({ appId }: { appId: string }) => {
/>
</Box>
{linkType === PublishChannelEnum.share && (
<Link appId={appId} type={PublishChannelEnum.share} />
)}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
</Box>
<Flex
flexDirection={'column'}
{...cardStyles}
boxShadow={3.5}
mt={4}
px={[4, 8]}
py={[4, 6]}
flex={'1 0 0'}
>
{linkType === PublishChannelEnum.share && (
<Link appId={appId} type={PublishChannelEnum.share} />
)}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
</Flex>
</>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react';
import { getPublishList, postRevertVersion } from '@/web/core/app/versionApi';
import { getPublishList, postRevertVersion } from '@/web/core/app/api/version';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next';
@@ -7,29 +7,38 @@ import { useMemoizedFn } from 'ahooks';
import { Box, Button, Flex } from '@chakra-ui/react';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from './context';
import { useI18n } from '@/web/context/I18n';
import { AppSchema } from '@fastgpt/global/core/app/type';
const PublishHistoriesSlider = () => {
export type InitProps = {
nodes: AppSchema['modules'];
edges: AppSchema['edges'];
chatConfig: AppSchema['chatConfig'];
};
const PublishHistoriesSlider = ({
onClose,
initData,
defaultData
}: {
onClose: () => void;
initData: (data: InitProps) => void;
defaultData: InitProps;
}) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { openConfirm, ConfirmModal } = useConfirm({
content: t('core.workflow.publish.OnRevert version confirm')
});
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
const appId = useContextSelector(WorkflowContext, (e) => e.appId);
const setIsShowVersionHistories = useContextSelector(
WorkflowContext,
(e) => e.setIsShowVersionHistories
);
const initData = useContextSelector(WorkflowContext, (e) => e.initData);
const appId = appDetail._id;
const [selectedHistoryId, setSelectedHistoryId] = useState<string>();
@@ -43,25 +52,25 @@ const PublishHistoriesSlider = () => {
}
});
const onClose = useMemoizedFn(() => {
setIsShowVersionHistories(false);
});
const onPreview = useCallback(
(data: AppVersionSchemaType) => {
setSelectedHistoryId(data._id);
const onPreview = useCallback((data: AppVersionSchemaType) => {
setSelectedHistoryId(data._id);
initData({
nodes: data.nodes,
edges: data.edges
});
}, []);
initData({
nodes: data.nodes,
edges: data.edges,
chatConfig: data.chatConfig
});
},
[initData]
);
const onCloseSlider = useCallback(
(data: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
(data: InitProps) => {
setSelectedHistoryId(undefined);
initData(data);
onClose();
},
[appDetail]
[initData, onClose]
);
const { mutate: onRevert, isLoading: isReverting } = useRequest({
@@ -69,8 +78,9 @@ const PublishHistoriesSlider = () => {
if (!appId) return;
await postRevertVersion(appId, {
versionId: data._id,
editNodes: appDetail.modules, // old workflow
editEdges: appDetail.edges
editNodes: defaultData.nodes, // old workflow
editEdges: defaultData.edges,
editChatConfig: defaultData.chatConfig
});
setAppDetail((state) => ({
@@ -90,8 +100,9 @@ const PublishHistoriesSlider = () => {
<CustomRightDrawer
onClose={() =>
onCloseSlider({
nodes: appDetail.modules,
edges: appDetail.edges
nodes: defaultData.nodes,
edges: defaultData.edges,
chatConfig: defaultData.chatConfig
})
}
iconSrc="core/workflow/versionHistories"
@@ -99,7 +110,7 @@ const PublishHistoriesSlider = () => {
maxW={'300px'}
px={0}
showMask={false}
mt={'60px'}
top={'60px'}
overflow={'unset'}
>
<Button
@@ -110,12 +121,13 @@ const PublishHistoriesSlider = () => {
onClick={() => {
setSelectedHistoryId(undefined);
initData({
nodes: appDetail.modules,
edges: appDetail.edges
nodes: defaultData.nodes,
edges: defaultData.edges,
chatConfig: defaultData.chatConfig
});
}}
>
{t('core.workflow.Current workflow')}
{appT('Current settings')}
</Button>
<ScrollList isLoading={showLoading} flex={'1 0 0'} px={5}>
{list.map((data, index) => {

View File

@@ -0,0 +1,75 @@
import { Box, HStack } from '@chakra-ui/react';
import React, { useCallback, useMemo } from 'react';
import { AppContext, TabEnum } from './context';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const RouteTab = () => {
const { t } = useTranslation();
const { appT } = useI18n();
const router = useRouter();
const { appDetail, currentTab } = useContextSelector(AppContext, (v) => v);
const setCurrentTab = useCallback(
(tab: TabEnum) => {
router.push({
query: {
...router.query,
currentTab: tab
}
});
},
[router]
);
const tabList = useMemo(
() => [
{
label: appDetail.type === AppTypeEnum.plugin ? appT('Setting plugin') : appT('Setting app'),
id: TabEnum.appEdit
},
...(appDetail.permission.hasManagePer
? [
{
label: appT('Publish channel'),
id: TabEnum.publish
},
{ label: appT('Chat logs'), id: TabEnum.logs }
]
: [])
],
[appDetail.permission.hasManagePer, appT]
);
return (
<HStack spacing={4} whiteSpace={'nowrap'}>
{tabList.map((tab) => (
<Box
key={tab.id}
px={2}
py={0.5}
{...(currentTab === tab.id
? {
color: 'primary.700'
}
: {
color: 'myGray.600',
cursor: 'pointer',
_hover: {
bg: 'myGray.200',
borderRadius: 'md'
},
onClick: () => setCurrentTab(tab.id)
})}
>
{tab.label}
</Box>
))}
</HStack>
);
};
export default RouteTab;

View File

@@ -0,0 +1,187 @@
import React, { useState } from 'react';
import {
Box,
Flex,
Button,
IconButton,
HStack,
Modal,
ModalBody,
Checkbox,
ModalFooter
} from '@chakra-ui/react';
import { DragHandleIcon } from '@chakra-ui/icons';
import { useRouter } from 'next/router';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { useTranslation } from 'next-i18next';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from '../TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppContext } from '@/pages/app/detail/components/context';
import { useContextSelector } from 'use-context-selector';
import PermissionIconText from '@/components/support/permission/IconText';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useI18n } from '@/web/context/I18n';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const AppCard = () => {
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail, setAppDetail, onOpenInfoEdit, onDelApp } = useContextSelector(
AppContext,
(v) => v
);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
// transition to workflow
const [transitionCreateNew, setTransitionCreateNew] = useState<boolean>();
const { runAsync: onTransition, loading: transiting } = useRequest2(
() => postTransition2Workflow({ appId, createNew: transitionCreateNew }),
{
onSuccess: ({ id }) => {
if (id) {
router.replace({
query: {
appId: id
}
});
} else {
setAppDetail((state) => ({
...state,
type: AppTypeEnum.workflow
}));
}
},
successToast: t('common.Success')
}
);
return (
<>
{/* basic info */}
<Box px={6} py={4} position={'relative'}>
<Flex alignItems={'center'}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'md'} flex={'1 0 0'}>
{appDetail.name}
</Box>
</Flex>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
minH={'46px'}
>
{appDetail.intro || t('core.app.tip.Add a intro to app')}
</Box>
<HStack>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() => router.push(`/chat?appId=${appId}`)}
>
{t('core.Chat')}
</Button>
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<Button
mr={3}
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<DragHandleIcon w={'16px'} />}
onClick={() => setTeamTagsSet(appDetail)}
>
{t('common.Team Tags Set')}
</Button>
)}
{appDetail.permission.hasManagePer && (
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
onClick={onOpenInfoEdit}
>
{t('common.Setting')}
</Button>
)}
{appDetail.permission.isOwner && (
<MyMenu
Button={
<IconButton
variant={'whiteBase'}
size={['smSquare', 'mdSquare']}
icon={<MyIcon name={'more'} w={'1rem'} />}
aria-label={''}
/>
}
menuList={[
{
children: [
{
icon: 'core/app/type/workflow',
label: appT('Transition to workflow'),
onClick: () => setTransitionCreateNew(true)
}
]
},
{
children: [
{
icon: 'delete',
type: 'danger',
label: t('common.Delete'),
onClick: onDelApp
}
]
}
]}
/>
)}
<Box flex={1} />
<MyTag
type="borderFill"
colorSchema="gray"
onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)}
>
<PermissionIconText defaultPermission={appDetail.defaultPermission} fontSize={'md'} />
</MyTag>
</HStack>
</Box>
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
{transitionCreateNew !== undefined && (
<MyModal isOpen title={appT('Transition to workflow')} iconSrc="core/app/type/workflow">
<ModalBody>
<Box mb={3}>{appT('Transition to workflow create new tip')}</Box>
<HStack cursor={'pointer'} onClick={() => setTransitionCreateNew((state) => !state)}>
<Checkbox isChecked={transitionCreateNew} />
<Box>{appT('Transition to workflow create new placeholder')}</Box>
</HStack>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={() => setTransitionCreateNew(undefined)} mr={3}>
{t('common.Close')}
</Button>
<Button variant={'dangerFill'} isLoading={transiting} onClick={() => onTransition()}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
)}
</>
);
};
export default React.memo(AppCard);

View File

@@ -0,0 +1,63 @@
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useEffect } from 'react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSafeState } from 'ahooks';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { useChatTest } from '../useChatTest';
const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
useEffect(() => {
const { nodes, edges } = form2AppWorkflow(appForm);
setWorkflowData({ nodes, edges });
}, [appForm, setWorkflowData]);
const { resetChatBox, ChatBox } = useChatTest({
...workflowData,
chatConfig: appForm.chatConfig
});
return (
<Flex position={'relative'} flexDirection={'column'} h={'100%'} py={4}>
<Flex px={[2, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1}>
{appT('Chat Debug')}
</Box>
<MyTooltip label={t('core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
resetChatBox();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox />
</Box>
</Flex>
);
};
export default React.memo(ChatTest);

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useMount } from 'ahooks';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
import ChatTest from './ChatTest';
import AppCard from './AppCard';
import EditForm from './EditForm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { AppContext } from '@/pages/app/detail/components/context';
import { useContextSelector } from 'use-context-selector';
import { cardStyles } from '../constants';
import styles from './styles.module.scss';
const Edit = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const { isPc } = useSystemStore();
const { loadAllDatasets } = useDatasetStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
// show selected dataset
useMount(() => {
loadAllDatasets();
setAppForm(
appWorkflow2Form({
nodes: appDetail.modules,
chatConfig: appDetail.chatConfig
})
);
if (appDetail.version !== 'v2') {
setAppForm(
appWorkflow2Form({
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
chatConfig: appDetail.chatConfig
})
);
}
});
return (
<Box
display={['block', 'flex']}
flex={'1 0 0'}
h={0}
pt={[2, 1.5]}
pl={[2, 1]}
gap={1}
borderRadius={'lg'}
overflowY={['auto', 'unset']}
>
<Box className={styles.EditAppBox} pr={[0, 1]} overflowY={'auto'} minW={'580px'} flex={'1'}>
<Box {...cardStyles} boxShadow={'2'}>
<AppCard />
</Box>
<Box mt={4} {...cardStyles} boxShadow={'3.5'} w={'auto'}>
<EditForm appForm={appForm} setAppForm={setAppForm} />
</Box>
</Box>
{isPc && (
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0}>
<ChatTest appForm={appForm} />
</Box>
)}
</Box>
);
};
export default React.memo(Edit);

View File

@@ -0,0 +1,480 @@
import React, { useEffect, useMemo, useTransition } from 'react';
import {
Box,
Flex,
Grid,
BoxProps,
useTheme,
useDisclosure,
Button,
HStack
} from '@chakra-ui/react';
import { AddIcon, SmallAddIcon } from '@chakra-ui/icons';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import dynamic from 'next/dynamic';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { getSystemVariables } from '@/web/core/app/utils';
import { useUpdate } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
const ToolSelectModal = dynamic(() => import('./components/ToolSelectModal'));
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 BoxStyles: BoxProps = {
px: 5,
py: '16px',
borderBottomWidth: '1px',
borderBottomColor: 'borderColor.low'
};
const LabelStyles: BoxProps = {
w: ['60px', '100px'],
flexShrink: 0,
fontSize: 'xs'
};
const EditForm = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { allDatasets } = useDatasetStore();
const { llmModelList } = useSystemStore();
const [, startTst] = useTransition();
const selectDatasets = useMemo(
() =>
allDatasets.filter((item) =>
appForm.dataset?.datasets.find((dataset) => dataset.datasetId === item._id)
),
[allDatasets, appForm?.dataset?.datasets]
);
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const {
isOpen: isOpenDatasetParams,
onOpen: onOpenDatasetParams,
onClose: onCloseDatasetParams
} = useDisclosure();
const {
isOpen: isOpenToolsSelect,
onOpen: onOpenToolsSelect,
onClose: onCloseToolsSelect
} = useDisclosure();
const formatVariables: any = useMemo(
() =>
formatEditorVariablePickerIcon([
...getSystemVariables(t),
...(appForm.chatConfig.variables || [])
]),
[appForm.chatConfig.variables, t]
);
const tokenLimit = useMemo(() => {
return (
llmModelList.find((item) => item.model === appForm.aiSettings.model)?.quoteMaxToken || 3000
);
}, [llmModelList, appForm.aiSettings.model]);
return (
<>
<Box>
{/* ai */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
<FormLabel ml={2} flex={1}>
{appT('AI Settings')}
</FormLabel>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box {...LabelStyles}>{t('core.ai.Model')}</Box>
<Box flex={'1 0 0'}>
<SettingLLMModel
llmModelType={'all'}
defaultData={{
model: appForm.aiSettings.model,
temperature: appForm.aiSettings.temperature,
maxToken: appForm.aiSettings.maxToken,
maxHistories: appForm.aiSettings.maxHistories
}}
onChange={({ model, temperature, maxToken, maxHistories }: SettingAIDataType) => {
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
model,
temperature,
maxToken,
maxHistories: maxHistories ?? 6
}
}));
}}
/>
</Box>
</Flex>
<Box mt={3}>
<HStack {...LabelStyles}>
<Box>{t('core.ai.Prompt')}</Box>
<QuestionTip label={t('core.app.tip.chatNodeSystemPromptTip')} />
</HStack>
<Box mt={1}>
<PromptEditor
value={appForm.aiSettings.systemPrompt}
onChange={(text) => {
startTst(() => {
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
systemPrompt: text
}
}));
});
}}
variables={formatVariables}
placeholder={t('core.app.tip.chatNodeSystemPromptTip')}
title={t('core.ai.Prompt')}
/>
</Box>
</Box>
</Box>
{/* dataset */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/simpleMode/dataset'} w={'20px'} />
<FormLabel ml={2}>{t('core.dataset.Choose Dataset')}</FormLabel>
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name="common/addLight" w={'0.8rem'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenKbSelect}
>
{t('common.Choose')}
</Button>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenDatasetParams}
>
{t('common.Params')}
</Button>
</Flex>
{appForm.dataset.datasets?.length > 0 && (
<Box my={3}>
<SearchParamsTip
searchMode={appForm.dataset.searchMode}
similarity={appForm.dataset.similarity}
limit={appForm.dataset.limit}
usingReRank={appForm.dataset.usingReRank}
queryExtensionModel={appForm.dataset.datasetSearchExtensionModel}
/>
</Box>
)}
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => (
<MyTooltip key={item._id} label={t('core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item._id
}
})
}
>
<Avatar src={item.avatar} w={'18px'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
{/* tool choice */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{t('core.app.Tool call')}()</FormLabel>
<QuestionTip ml={1} label={t('core.app.Tool call tip')} />
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={onOpenToolsSelect}
>
{t('common.Choose')}
</Button>
</Flex>
<Grid
mt={appForm.selectedTools.length > 0 ? 2 : 0}
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={[2, 4]}
>
{appForm.selectedTools.map((item) => (
<MyTooltip key={item.id} label={item.intro}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2.5}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
_hover={{
...hoverDeleteStyles,
borderColor: 'primary.300'
}}
>
<Avatar src={item.avatar} w={'1rem'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
<DeleteIcon
onClick={() => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((tool) => tool.id !== item.id)
}));
}}
/>
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
{/* variable */}
<Box {...BoxStyles}>
<VariableEdit
variables={appForm.chatConfig.variables}
onChange={(e) => {
appForm.chatConfig.variables = e;
}}
/>
</Box>
{/* welcome */}
<Box {...BoxStyles}>
<WelcomeTextConfig
value={appForm.chatConfig.welcomeText}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
welcomeText: e.target.value
}
}));
}}
/>
</Box>
{/* tts */}
<Box {...BoxStyles}>
<TTSSelect
value={appForm.chatConfig.ttsConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
ttsConfig: e
}
}));
}}
/>
</Box>
{/* whisper */}
<Box {...BoxStyles}>
<WhisperConfig
isOpenAudio={appForm.chatConfig.ttsConfig?.type !== TTSTypeEnum.none}
value={appForm.chatConfig.whisperConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
whisperConfig: e
}
}));
}}
/>
</Box>
{/* question guide */}
<Box {...BoxStyles}>
<QGSwitch
isChecked={appForm.chatConfig.questionGuide}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
questionGuide: e.target.checked
}
}));
}}
/>
</Box>
{/* question tips */}
<Box {...BoxStyles}>
<InputGuideConfig
appId={appDetail._id}
value={appForm.chatConfig.chatInputGuide}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
chatInputGuide: e
}
}));
}}
/>
</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 && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={selectDatasets.map((item) => ({
datasetId: item._id,
vectorModel: item.vectorModel
}))}
onClose={onCloseKbSelect}
onChange={(e) => {
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
datasets: e
}
}));
}}
/>
)}
{isOpenDatasetParams && (
<DatasetParamsModal
{...appForm.dataset}
maxTokens={tokenLimit}
onClose={onCloseDatasetParams}
onSuccess={(e) => {
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
...e
}
}));
}}
/>
)}
{isOpenToolsSelect && (
<ToolSelectModal
selectedTools={appForm.selectedTools}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: [...state.selectedTools, e]
}));
}}
onRemoveTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((item) => item.id !== e.id)
}));
}}
onClose={onCloseToolsSelect}
/>
)}
</>
);
};
export default React.memo(EditForm);

View File

@@ -0,0 +1,164 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import FolderPath from '@/components/common/folder/Path';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppFolderPath } from '@/web/core/app/api/app';
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import RouteTab from '../RouteTab';
import { useTranslation } from 'next-i18next';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { TabEnum } from '../context';
import PublishHistoriesSlider, { type InitProps } from '../PublishHistoriesSlider';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { compareWorkflow } from '@/web/core/workflow/utils';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
const Header = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const { t } = useTranslation();
const { isPc } = useSystemStore();
const router = useRouter();
const { appId, appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), {
manual: false,
refreshDeps: [appId]
});
const onclickRoute = useCallback(
(parentId: string) => {
router.push({
pathname: '/app/list',
query: {
parentId
}
});
},
[router]
);
const isPublished = useMemo(() => {
const data = form2AppWorkflow(appForm);
return compareWorkflow(
{
nodes: appDetail.modules,
edges: [],
chatConfig: appDetail.chatConfig
},
{
nodes: data.nodes,
edges: [],
chatConfig: data.chatConfig
}
);
}, [appDetail.chatConfig, appDetail.modules, appForm]);
const onSubmitPublish = useCallback(
async (data: AppSimpleEditFormType) => {
const { nodes, edges } = form2AppWorkflow(data);
await onPublish({
nodes,
edges,
chatConfig: data.chatConfig,
type: AppTypeEnum.simple
});
},
[onPublish]
);
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
return (
<Box>
{!isPc && (
<Flex pt={2} justifyContent={'center'}>
<RouteTab />
</Flex>
)}
<Flex pl={2} pt={[2, 3]} alignItems={'flex-start'} position={'relative'}>
<Box flex={'1'}>
<FolderPath paths={paths} hoverStyle={{ color: 'primary.600' }} onClick={onclickRoute} />
</Box>
{isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)}
{currentTab === TabEnum.appEdit && (
<Flex alignItems={'center'}>
{!historiesDefaultData && (
<>
<MyTag
mr={3}
type={'borderFill'}
showDot
colorSchema={
isPublished
? publishStatusStyle.published.colorSchema
: publishStatusStyle.unPublish.colorSchema
}
>
{isPublished
? publishStatusStyle.published.text
: publishStatusStyle.unPublish.text}
</MyTag>
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => {
const { nodes, edges } = form2AppWorkflow(appForm);
setHistoriesDefaultData({
nodes,
edges,
chatConfig: appForm.chatConfig
});
}}
/>
<PopoverConfirm
showCancel
content={t('core.app.Publish Confirm')}
Trigger={<Button isDisabled={isPublished}>{t('core.app.Publish')}</Button>}
onConfirm={() => onSubmitPublish(appForm)}
/>
</>
)}
</Flex>
)}
</Flex>
{!!historiesDefaultData && (
<PublishHistoriesSlider
initData={({ nodes, chatConfig }) => {
setAppForm(
appWorkflow2Form({
nodes,
chatConfig
})
);
}}
onClose={() => setHistoriesDefaultData(undefined)}
defaultData={historiesDefaultData}
/>
)}
</Box>
);
};
export default Header;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
@@ -20,24 +20,25 @@ import {
Textarea
} from '@chakra-ui/react';
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useQuery } from '@tanstack/react-query';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { AddIcon } from '@chakra-ui/icons';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
import { getPreviewPluginNode, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import ParentPaths from '@/components/common/ParentPaths';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { debounce } from 'lodash';
import { useForm } from 'react-hook-form';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { getAppFolderPath } from '@/web/core/app/api/app';
import FolderPath from '@/components/common/folder/Path';
type Props = {
selectedTools: FlowNodeTemplateType[];
@@ -52,55 +53,38 @@ enum TemplateTypeEnum {
const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) => {
const { t } = useTranslation();
const {
systemNodeTemplates,
loadSystemNodeTemplates,
teamPluginNodeTemplates,
loadTeamPluginNodeTemplates
} = useWorkflowStore();
const [templateType, setTemplateType] = useState(TemplateTypeEnum.teamPlugin);
const [currentParent, setCurrentParent] = useState<{
parentId: string;
parentName: string;
}>();
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const templates = useMemo(() => {
const map = {
[TemplateTypeEnum.systemPlugin]: systemNodeTemplates.filter(
(item) => item.isTool && item.name.toLowerCase().includes(searchKey.toLowerCase())
),
[TemplateTypeEnum.teamPlugin]: teamPluginNodeTemplates.filter((item) =>
searchKey ? item.pluginType !== PluginTypeEnum.folder : true
)
};
return map[templateType];
}, [searchKey, systemNodeTemplates, teamPluginNodeTemplates, templateType]);
const { mutate: onChangeTab } = useRequest({
mutationFn: async (e: any) => {
const val = e as TemplateTypeEnum;
if (val === TemplateTypeEnum.systemPlugin) {
await loadSystemNodeTemplates();
} else if (val === TemplateTypeEnum.teamPlugin) {
await loadTeamPluginNodeTemplates({
parentId: currentParent?.parentId
const { data: templates = [], loading: isLoading } = useRequest2(
async () => {
if (templateType === TemplateTypeEnum.systemPlugin) {
return (await getSystemPlugTemplates()).filter(
(item) => item.isTool && item.name.toLowerCase().includes(searchKey.toLowerCase())
);
} else if (templateType === TemplateTypeEnum.teamPlugin) {
return getTeamPlugTemplates({
parentId,
searchKey,
type: [AppTypeEnum.folder, AppTypeEnum.httpPlugin, AppTypeEnum.plugin]
});
}
setTemplateType(val);
},
errorToast: t('core.module.templates.Load plugin error')
});
const { isLoading } = useQuery(['teamNodeTemplate', currentParent?.parentId, searchKey], () =>
loadTeamPluginNodeTemplates({
parentId: currentParent?.parentId,
searchKey,
init: true
})
{
manual: false,
throttleWait: 300,
refreshDeps: [templateType, searchKey, parentId],
errorToast: t('core.module.templates.Load plugin error')
}
);
const { data: paths = [] } = useRequest2(() => getAppFolderPath(parentId), {
manual: false,
refreshDeps: [parentId]
});
return (
<MyModal
isOpen
@@ -129,7 +113,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
py={'5px'}
px={'15px'}
value={templateType}
onChange={onChangeTab}
onChange={(e) => setTemplateType(e as TemplateTypeEnum)}
/>
<InputGroup w={300}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
@@ -138,20 +122,19 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
<Input
bg={'myGray.50'}
placeholder={t('plugin.Search plugin')}
onChange={debounce((e) => setSearchKey(e.target.value), 200)}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
</Box>
{/* route components */}
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && currentParent && (
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && parentId && (
<Flex mt={2} px={[3, 6]}>
<ParentPaths
paths={[currentParent]}
<FolderPath
paths={paths}
FirstPathDom={null}
onClick={() => {
setCurrentParent(undefined);
setParentId(null);
}}
fontSize="md"
/>
</Flex>
)}
@@ -159,7 +142,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
<RenderList
templates={templates}
isLoadingData={isLoading}
setCurrentParent={setCurrentParent}
setParentId={setParentId}
{...props}
/>
</MyBox>
@@ -175,11 +158,11 @@ const RenderList = React.memo(function RenderList({
isLoadingData,
onAddTool,
onRemoveTool,
setCurrentParent
setParentId
}: Props & {
templates: FlowNodeTemplateType[];
isLoadingData: boolean;
setCurrentParent: (e: { parentId: string; parentName: string }) => void;
setParentId: React.Dispatch<React.SetStateAction<ParentIdType>>;
}) {
const { t } = useTranslation();
const [configTool, setConfigTool] = useState<FlowNodeTemplateType>();
@@ -204,7 +187,7 @@ const RenderList = React.memo(function RenderList({
const { mutate: onClickAdd, isLoading } = useRequest({
mutationFn: async (template: FlowNodeTemplateType) => {
const res = await getPreviewPluginModule(template.id);
const res = await getPreviewPluginNode({ appId: template.id });
if (!checkToolInputValid(res)) {
return Promise.reject(t('core.app.ToolCall.This plugin cannot be called as a tool'));
@@ -250,7 +233,7 @@ const RenderList = React.memo(function RenderList({
<Box ml={5} flex={'1 0 0'}>
<Box color={'black'}>{t(item.name)}</Box>
{item.intro && (
<Box className="textEllipsis3" color={'myGray.500'} fontSize={['xs', 'sm']}>
<Box className="textEllipsis3" color={'myGray.500'} fontSize={'xs'}>
{t(item.intro)}
</Box>
)}
@@ -264,12 +247,9 @@ const RenderList = React.memo(function RenderList({
>
{t('common.Remove')}
</Button>
) : item.pluginType === PluginTypeEnum.folder ? (
<Button
size={'sm'}
variant={'whiteBase'}
onClick={() => setCurrentParent({ parentId: item.id, parentName: item.name })}
>
) : item.pluginType === PluginTypeEnum.folder ||
item.pluginType === AppTypeEnum.httpPlugin ? (
<Button size={'sm'} variant={'whiteBase'} onClick={() => setParentId(item.id)}>
{t('common.Open')}
</Button>
) : (

View File

@@ -0,0 +1,34 @@
import React, { useState } from 'react';
import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import Header from './Header';
import Edit from './Edit';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import dynamic from 'next/dynamic';
import { Flex } from '@chakra-ui/react';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const SimpleEdit = () => {
const { currentTab } = useContextSelector(AppContext, (v) => v);
const [appForm, setAppForm] = useState(getDefaultAppForm());
return (
<Flex h={'100%'} flexDirection={'column'} pr={3} pb={3}>
<Header appForm={appForm} setAppForm={setAppForm} />
{currentTab === TabEnum.appEdit ? (
<Edit appForm={appForm} setAppForm={setAppForm} />
) : (
<Flex h={'100%'} flexDirection={'column'} mt={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
)}
</Flex>
);
};
export default React.memo(SimpleEdit);

View File

@@ -0,0 +1,10 @@
.EditAppBox {
&::-webkit-scrollbar-thumb {
background: #dfe2ea !important;
transition: background 1s;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--chakra-colors-gray-300) !important;
}
}

View File

@@ -1,175 +0,0 @@
import React, { useState } from 'react';
import { Box, Flex, Button, IconButton, useDisclosure } from '@chakra-ui/react';
import { DragHandleIcon } from '@chakra-ui/icons';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRouter } from 'next/router';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { delAppById } from '@/web/core/app/api';
import { useTranslation } from 'next-i18next';
import PermissionIconText from '@/components/support/permission/IconText';
import dynamic from 'next/dynamic';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from './TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useI18n } from '@/web/context/I18n';
import { AppContext } from '@/web/core/app/context/appContext';
import { useContextSelector } from 'use-context-selector';
const InfoModal = dynamic(() => import('../InfoModal'));
const AppCard = () => {
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { toast } = useToast();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
const {
isOpen: isOpenInfoEdit,
onOpen: onOpenInfoEdit,
onClose: onCloseInfoEdit
} = useDisclosure();
const { openConfirm: openConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({
content: appT('Confirm Del App Tip'),
type: 'delete'
});
/* 点击删除 */
const { mutate: handleDelModel, isLoading } = useRequest({
mutationFn: async () => {
if (!appDetail) return null;
await delAppById(appDetail._id);
return 'success';
},
onSuccess(res) {
if (!res) return;
toast({
title: t('common.Delete Success'),
status: 'success'
});
router.replace(`/app/list`);
},
errorToast: t('common.Delete Failed')
});
return (
<>
<Box px={4}>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
<PermissionIconText defaultPermission={appDetail.defaultPermission} fontSize={'md'} />
</Box>
<Box color={'myGray.500'} fontSize={'xs'}>
AppId:{' '}
<Box as={'span'} userSelect={'all'}>
{appId}
</Box>
</Box>
</Flex>
{/* basic info */}
<Box
borderWidth={'1px'}
borderColor={'primary.1'}
borderRadius={'md'}
mt={2}
px={5}
py={4}
bg={'primary.50'}
position={'relative'}
>
<Flex alignItems={'center'}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'md'}>
{appDetail.name}
</Box>
{appDetail.permission.isOwner && (
<IconButton
className="delete"
position={'absolute'}
top={4}
right={4}
size={'smSquare'}
icon={<MyIcon name={'delete'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
isLoading={isLoading}
onClick={openConfirmDel(handleDelModel)}
/>
)}
</Flex>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
>
{appDetail.intro || t('core.app.tip.Add a intro to app')}
</Box>
<Flex>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() => router.push(`/chat?appId=${appId}`)}
>
{t('core.Chat')}
</Button>
<Button
mx={3}
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'support/outlink/shareLight'} w={'16px'} />}
onClick={() => {
router.replace({
query: {
appId,
currentTab: 'publish'
}
});
}}
>
{t('core.app.navbar.Publish')}
</Button>
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<Button
mr={3}
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<DragHandleIcon w={'16px'} />}
onClick={() => setTeamTagsSet(appDetail)}
>
{t('common.Team Tags Set')}
</Button>
)}
{appDetail.permission.hasManagePer && (
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
onClick={onOpenInfoEdit}
>
{t('common.Setting')}
</Button>
)}
</Flex>
</Box>
</Box>
<ConfirmDelModal />
{isOpenInfoEdit && <InfoModal onClose={onCloseInfoEdit} />}
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
</>
);
};
export default React.memo(AppCard);

View File

@@ -1,160 +0,0 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useCallback, useEffect, useRef } from 'react';
import ChatBox from '@/components/ChatBox';
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
import { streamFetch } from '@/web/common/api/fetch';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import {
getDefaultEntryNodeIds,
getMaxHistoryLimitFromNodes,
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { useMemoizedFn, useSafeState } from 'ahooks';
import { UseFormReturn } from 'react-hook-form';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
const ChatTest = ({
editForm,
appId
}: {
editForm: UseFormReturn<AppSimpleEditFormType, any>;
appId: string;
}) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { userInfo } = useUserStore();
const ChatBoxRef = useRef<ComponentRef>(null);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { watch } = editForm;
const chatConfig = watch('chatConfig');
const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
const startChat = useMemoizedFn(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
if (!workflowData) return Promise.reject('workflowData is empty');
/* get histories */
let historyMaxLen = getMaxHistoryLimitFromNodes(workflowData.nodes);
const history = chatList.slice(-historyMaxLen - 2, -2);
// 流请求,获取数据
const { responseText, responseData } = await streamFetch({
url: '/api/core/chat/chatTest',
data: {
history,
prompt: chatList[chatList.length - 2].value,
nodes: storeNodes2RuntimeNodes(
workflowData.nodes,
getDefaultEntryNodeIds(workflowData.nodes)
),
edges: initWorkflowEdgeStatus(workflowData.edges),
variables,
appId,
appName: `调试-${appDetail.name}`
},
onMessage: generatingMessage,
abortCtrl: controller
});
return { responseText, responseData };
}
);
const resetChatBox = useCallback(() => {
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}, []);
useEffect(() => {
const wat = watch((data) => {
const { nodes, edges } = form2AppWorkflow(data as AppSimpleEditFormType);
setWorkflowData({ nodes, edges });
});
return () => {
wat.unsubscribe();
};
}, [setWorkflowData, watch]);
return (
<Flex
position={'relative'}
flexDirection={'column'}
h={'100%'}
py={4}
overflowX={'auto'}
bg={'white'}
>
<Flex px={[2, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1}>
{appT('Chat Debug')}
</Box>
<MyTooltip label={t('core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
resetChatBox();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appId={appDetail._id}
appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
chatConfig={chatConfig}
showFileSelector={checkChatSupportSelectFileByModules(workflowData.nodes)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
</Box>
{appDetail.type !== AppTypeEnum.simple && (
<Flex
position={'absolute'}
top={0}
right={0}
left={0}
bottom={0}
bg={'rgba(255,255,255,0.7)'}
alignItems={'center'}
justifyContent={'center'}
flexDirection={'column'}
fontSize={'lg'}
color={'black'}
whiteSpace={'pre-wrap'}
textAlign={'center'}
>
<Box fontSize={'md'}>{appT('Advance App TestTip')}</Box>
</Flex>
)}
</Flex>
);
};
export default React.memo(ChatTest);

View File

@@ -1,521 +0,0 @@
import React, { useEffect, useMemo, useTransition } from 'react';
import {
Box,
Flex,
Grid,
BoxProps,
useTheme,
useDisclosure,
Button,
HStack
} from '@chakra-ui/react';
import { AddIcon, SmallAddIcon } from '@chakra-ui/icons';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { form2AppWorkflow } from '@/web/core/app/utils';
import dynamic from 'next/dynamic';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit';
import MyTextarea from '@/components/common/Textarea/MyTextarea/index';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { getSystemVariables } from '@/web/core/app/utils';
import { useUpdate } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
const ToolSelectModal = dynamic(() => import('./ToolSelectModal'));
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 BoxStyles: BoxProps = {
px: 5,
py: '16px',
borderBottomWidth: '1px',
borderBottomColor: 'borderColor.low'
};
const LabelStyles: BoxProps = {
w: ['60px', '100px'],
flexShrink: 0,
fontSize: 'xs'
};
const EditForm = ({
editForm,
divRef,
isSticky
}: {
editForm: UseFormReturn<AppSimpleEditFormType, any>;
divRef: React.RefObject<HTMLDivElement>;
isSticky: boolean;
}) => {
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail, publishApp } = useContextSelector(AppContext, (v) => v);
const { allDatasets } = useDatasetStore();
const { llmModelList } = useSystemStore();
const [, startTst] = useTransition();
const refresh = useUpdate();
const { setValue, getValues, handleSubmit, control, watch } = editForm;
const { fields: datasets, replace: replaceDatasetList } = useFieldArray({
control,
name: 'dataset.datasets'
});
const selectDatasets = useMemo(
() => allDatasets.filter((item) => datasets.find((dataset) => dataset.datasetId === item._id)),
[allDatasets, datasets]
);
// useEffect(() => {
// if (selectDatasets.length !== datasets.length) {
// replaceDatasetList(
// selectDatasets.map((item) => ({
// datasetId: item._id
// }))
// );
// }
// }, [datasets, replaceDatasetList, selectDatasets]);
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const {
isOpen: isOpenDatasetParams,
onOpen: onOpenDatasetParams,
onClose: onCloseDatasetParams
} = useDisclosure();
const {
isOpen: isOpenToolsSelect,
onOpen: onOpenToolsSelect,
onClose: onCloseToolsSelect
} = useDisclosure();
const { openConfirm: openConfirmSave, ConfirmModal: ConfirmSaveModal } = useConfirm({
content: t('core.app.edit.Confirm Save App Tip')
});
const aiSystemPrompt = watch('aiSettings.systemPrompt');
const selectLLMModel = watch('aiSettings.model');
const datasetSearchSetting = watch('dataset');
const variables = watch('chatConfig.variables');
const formatVariables: any = useMemo(
() => formatEditorVariablePickerIcon([...getSystemVariables(t), ...(variables || [])]),
[t, variables]
);
const tts = getValues('chatConfig.ttsConfig');
const whisperConfig = getValues('chatConfig.whisperConfig');
const postQuestionGuide = getValues('chatConfig.questionGuide');
const selectedTools = watch('selectedTools');
const inputGuideConfig = watch('chatConfig.chatInputGuide');
const scheduledTriggerConfig = watch('chatConfig.scheduledTriggerConfig');
const searchMode = watch('dataset.searchMode');
const tokenLimit = useMemo(() => {
return llmModelList.find((item) => item.model === selectLLMModel)?.quoteMaxToken || 3000;
}, [selectLLMModel, llmModelList]);
/* on save app */
const { mutate: onSubmitPublish, isLoading: isSaving } = useRequest({
mutationFn: async (data: AppSimpleEditFormType) => {
const { nodes, edges } = form2AppWorkflow(data);
await publishApp({
nodes,
edges,
chatConfig: data.chatConfig,
type: AppTypeEnum.simple
});
},
successToast: t('common.Save Success'),
errorToast: t('common.Save Failed')
});
useEffect(() => {
const wat = watch((data) => {
refresh();
});
return () => {
wat.unsubscribe();
};
}, []);
return (
<Box>
{/* title */}
<Flex
ref={divRef}
position={'sticky'}
top={-4}
bg={'myGray.25'}
py={4}
justifyContent={'space-between'}
alignItems={'center'}
zIndex={100}
px={4}
{...(isSticky && {
borderBottom: theme.borders.base,
boxShadow: '0 2px 10px rgba(0,0,0,0.12)'
})}
>
<HStack>
<Box color={'myGray.900'}>{t('core.app.App params config')}</Box>
<QuestionTip label={t('core.app.Simple Config Tip')} />
</HStack>
<Button
isLoading={isSaving}
size={['sm', 'md']}
leftIcon={
appDetail.type === AppTypeEnum.simple ? (
<MyIcon name={'common/publishFill'} w={['14px', '16px']} />
) : undefined
}
variant={appDetail.type === AppTypeEnum.simple ? 'primary' : 'whitePrimary'}
onClick={() => {
if (appDetail.type !== AppTypeEnum.simple) {
openConfirmSave(handleSubmit((data) => onSubmitPublish(data)))();
} else {
handleSubmit((data) => onSubmitPublish(data))();
}
}}
>
{appDetail.type !== AppTypeEnum.simple
? t('core.app.Change to simple mode')
: t('core.app.Publish')}
</Button>
</Flex>
<Box px={4}>
<Box bg={'white'} borderRadius={'md'} borderWidth={'1px'} borderColor={'borderColor.base'}>
{/* ai */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
<FormLabel ml={2} flex={1}>
{appT('AI Settings')}
</FormLabel>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box {...LabelStyles}>{t('core.ai.Model')}</Box>
<Box flex={'1 0 0'}>
<SettingLLMModel
llmModelType={'all'}
defaultData={{
model: getValues('aiSettings.model'),
temperature: getValues('aiSettings.temperature'),
maxToken: getValues('aiSettings.maxToken'),
maxHistories: getValues('aiSettings.maxHistories')
}}
onChange={({ model, temperature, maxToken, maxHistories }: SettingAIDataType) => {
setValue('aiSettings.model', model);
setValue('aiSettings.maxToken', maxToken);
setValue('aiSettings.temperature', temperature);
setValue('aiSettings.maxHistories', maxHistories ?? 6);
}}
/>
</Box>
</Flex>
<Box mt={3}>
<HStack {...LabelStyles}>
<Box>{t('core.ai.Prompt')}</Box>
<QuestionTip label={t('core.app.tip.chatNodeSystemPromptTip')} />
</HStack>
<Box mt={1}>
<PromptEditor
value={aiSystemPrompt}
onChange={(text) => {
startTst(() => {
setValue('aiSettings.systemPrompt', text);
});
}}
variables={formatVariables}
placeholder={t('core.app.tip.chatNodeSystemPromptTip')}
title={t('core.ai.Prompt')}
/>
</Box>
</Box>
</Box>
{/* dataset */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/simpleMode/dataset'} w={'20px'} />
<FormLabel ml={2}>{t('core.dataset.Choose Dataset')}</FormLabel>
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenKbSelect}
>
{t('common.Choose')}
</Button>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenDatasetParams}
>
{t('common.Params')}
</Button>
</Flex>
{datasetSearchSetting.datasets?.length > 0 && (
<Box my={3}>
<SearchParamsTip
searchMode={searchMode}
similarity={getValues('dataset.similarity')}
limit={getValues('dataset.limit')}
usingReRank={getValues('dataset.usingReRank')}
queryExtensionModel={getValues('dataset.datasetSearchExtensionModel')}
/>
</Box>
)}
<Grid
gridTemplateColumns={['repeat(2, minmax(0, 1fr))', 'repeat(3, minmax(0, 1fr))']}
gridGap={[2, 4]}
>
{selectDatasets.map((item) => (
<MyTooltip key={item._id} label={t('core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item._id
}
})
}
>
<Avatar src={item.avatar} w={'18px'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
{/* tool choice */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{t('core.app.Tool call')}()</FormLabel>
<QuestionTip ml={1} label={t('core.app.Tool call tip')} />
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={onOpenToolsSelect}
>
{t('common.Choose')}
</Button>
</Flex>
<Grid
mt={selectedTools.length > 0 ? 2 : 0}
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={[2, 4]}
>
{selectedTools.map((item) => (
<Flex
key={item.id}
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
_hover={{
...hoverDeleteStyles,
borderColor: 'primary.300'
}}
>
<Avatar src={item.avatar} w={'18px'} mr={1} />
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
{item.name}
</Box>
<DeleteIcon
onClick={() => {
setValue(
'selectedTools',
selectedTools.filter((tool) => tool.id !== item.id)
);
}}
/>
</Flex>
))}
</Grid>
</Box>
{/* variable */}
<Box {...BoxStyles}>
<VariableEdit
variables={variables}
onChange={(e) => {
setValue('chatConfig.variables', e);
}}
/>
</Box>
{/* welcome */}
<Box {...BoxStyles}>
<WelcomeTextConfig
defaultValue={getValues('chatConfig.welcomeText')}
onBlur={(e) => {
setValue('chatConfig.welcomeText', e.target.value || '');
}}
/>
</Box>
{/* tts */}
<Box {...BoxStyles}>
<TTSSelect
value={tts}
onChange={(e) => {
setValue('chatConfig.ttsConfig', e);
}}
/>
</Box>
{/* whisper */}
<Box {...BoxStyles}>
<WhisperConfig
isOpenAudio={tts?.type !== TTSTypeEnum.none}
value={whisperConfig}
onChange={(e) => {
setValue('chatConfig.whisperConfig', e);
}}
/>
</Box>
{/* question guide */}
<Box {...BoxStyles}>
<QGSwitch
isChecked={postQuestionGuide}
onChange={(e) => {
setValue('chatConfig.questionGuide', e.target.checked);
}}
/>
</Box>
{/* question tips */}
<Box {...BoxStyles}>
<InputGuideConfig
appId={appDetail._id}
value={inputGuideConfig}
onChange={(e) => {
setValue('chatConfig.chatInputGuide', e);
}}
/>
</Box>
{/* timer trigger */}
<Box {...BoxStyles} borderBottom={'none'}>
<ScheduledTriggerConfig
value={scheduledTriggerConfig}
onChange={(e) => {
setValue('chatConfig.scheduledTriggerConfig', e);
}}
/>
</Box>
</Box>
</Box>
<ConfirmSaveModal bg={appDetail.type === AppTypeEnum.simple ? '' : 'red.600'} countDown={5} />
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={selectDatasets.map((item) => ({
datasetId: item._id,
vectorModel: item.vectorModel
}))}
onClose={onCloseKbSelect}
onChange={replaceDatasetList}
/>
)}
{isOpenDatasetParams && (
<DatasetParamsModal
{...datasetSearchSetting}
maxTokens={tokenLimit}
onClose={onCloseDatasetParams}
onSuccess={(e) => {
setValue('dataset', {
...getValues('dataset'),
...e
});
}}
/>
)}
{isOpenToolsSelect && (
<ToolSelectModal
selectedTools={selectedTools}
onAddTool={(e) => {
setValue('selectedTools', [...selectedTools, e]);
}}
onRemoveTool={(e) => {
setValue(
'selectedTools',
selectedTools.filter((item) => item.pluginId !== e.pluginId)
);
}}
onClose={onCloseToolsSelect}
/>
)}
</Box>
);
};
export default React.memo(EditForm);

View File

@@ -1,70 +0,0 @@
import React from 'react';
import { Box, Grid } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useSticky } from '@/web/common/hooks/useSticky';
import { useMount } from 'ahooks';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { useForm } from 'react-hook-form';
import { appWorkflow2Form, getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import ChatTest from './ChatTest';
import AppCard from './AppCard';
import EditForm from './EditForm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { AppContext } from '@/web/core/app/context/appContext';
import { useContextSelector } from 'use-context-selector';
const SimpleEdit = ({ appId }: { appId: string }) => {
const { isPc } = useSystemStore();
const { parentRef, divRef, isSticky } = useSticky();
const { loadAllDatasets } = useDatasetStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const editForm = useForm<AppSimpleEditFormType>({
defaultValues: getDefaultAppForm()
});
// show selected dataset
useMount(() => {
loadAllDatasets();
editForm.reset(
appWorkflow2Form({
nodes: appDetail.modules,
chatConfig: appDetail.chatConfig
})
);
if (appDetail.version !== 'v2') {
editForm.reset(
appWorkflow2Form({
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
chatConfig: appDetail.chatConfig
})
);
}
});
return (
<Grid gridTemplateColumns={['1fr', '560px 1fr']} h={'100%'}>
<Box
ref={parentRef}
h={'100%'}
borderRight={'1.5px solid'}
borderColor={'myGray.200'}
pt={[0, 4]}
pb={10}
overflow={'overlay'}
>
<AppCard />
<Box mt={2}>
<EditForm editForm={editForm} divRef={divRef} isSticky={isSticky} />
</Box>
</Box>
{isPc && <ChatTest editForm={editForm} appId={appId} />}
</Grid>
);
};
export default React.memo(SimpleEdit);

View File

@@ -23,7 +23,7 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { getTeamsTags } from '@/web/support/user/team/api';
import { useQuery } from '@tanstack/react-query';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
const TagsEditModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();

View File

@@ -0,0 +1,198 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
import { useInterval } from 'ahooks';
import { AppContext, TabEnum } from '../context';
import RouteTab from '../RouteTab';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard';
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
const Header = () => {
const { t } = useTranslation();
const { isPc } = useSystemStore();
const router = useRouter();
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const {
flowData2StoreDataAndCheck,
setWorkflowTestData,
onSaveWorkflow,
setHistoriesDefaultData,
historiesDefaultData,
initData
} = useContextSelector(WorkflowContext, (v) => v);
const onclickPublish = useCallback(async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
await onPublish({
...data,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
}
}, [flowData2StoreDataAndCheck, onPublish, appDetail.chatConfig]);
const saveAndBack = useCallback(async () => {
try {
await onSaveWorkflow();
router.push('/app/list');
} catch (error) {}
}, [onSaveWorkflow, router]);
// effect
useBeforeunload({
callback: onSaveWorkflow,
tip: t('core.common.tip.leave page')
});
useInterval(() => {
if (!appDetail._id) return;
onSaveWorkflow();
}, 40000);
const Render = useMemo(() => {
return (
<>
{!isPc && (
<Flex pt={2} justifyContent={'center'}>
<RouteTab />
</Flex>
)}
<Flex
mt={[2, 0]}
py={3}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
alignItems={'center'}
userSelect={'none'}
h={'67px'}
{...(currentTab === TabEnum.appEdit
? {
bg: 'myGray.25'
}
: {
bg: 'transparent',
borderBottomColor: 'transparent'
})}
>
{/* back */}
<MyIcon
name={'common/leftArrowLight'}
w={'1.75rem'}
cursor={'pointer'}
onClick={saveAndBack}
/>
{/* app info */}
<Box ml={1}>
<AppCard
showSaveStatus={
!historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit
}
/>
</Box>
{isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)}
<Box flex={1} />
{currentTab === TabEnum.appEdit && (
<>
{!historiesDefaultData && (
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={async () => {
const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore());
setHistoriesDefaultData({
nodes,
edges,
chatConfig: appDetail.chatConfig
});
}}
/>
)}
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('core.workflow.Debug')}
</Button>
{!historiesDefaultData && (
<PopoverConfirm
showCancel
content={t('core.app.Publish Confirm')}
Trigger={
<Button
ml={[2, 4]}
size={'sm'}
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
>
{t('core.app.Publish')}
</Button>
}
onConfirm={() => onclickPublish()}
/>
)}
</>
)}
</Flex>
{historiesDefaultData && (
<PublishHistories
initData={initData}
onClose={() => {
setHistoriesDefaultData(undefined);
}}
defaultData={historiesDefaultData}
/>
)}
</>
);
}, [
isPc,
currentTab,
saveAndBack,
historiesDefaultData,
isV2Workflow,
t,
initData,
setHistoriesDefaultData,
appDetail.chatConfig,
flowData2StoreDataAndCheck,
setWorkflowTestData,
onclickPublish
]);
return Render;
};
export default React.memo(Header);

View File

@@ -1,21 +1,25 @@
import React, { useEffect, useMemo } from 'react';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import Header from './Header';
import Flow from '@/components/core/workflow/Flow';
import React from 'react';
import { appSystemModuleTemplates } 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 '@/components/core/workflow/context';
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
import Header from './Header';
import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
type Props = { onClose: () => void };
const Render = ({ onClose }: Props) => {
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
import Flow from '../WorkflowComponents/Flow';
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 { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
content:
@@ -24,47 +28,45 @@ const Render = ({ onClose }: Props) => {
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
const workflowStringData = JSON.stringify({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
useMount(() => {
if (!isV2Workflow) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
})();
} else {
initData(JSON.parse(workflowStringData));
initData(
cloneDeep({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
})
);
}
});
const memoRender = useMemo(() => {
return <Flow Header={<Header onClose={onClose} />} />;
}, [onClose]);
return (
<>
{memoRender}
<Flex {...workflowBoxStyles}>
<Header />
{currentTab === TabEnum.appEdit ? (
<Flow />
) : (
<Flex flexDirection={'column'} h={'100%'} px={4} pb={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
)}
{!isV2Workflow && <ConfirmModal countDown={0} />}
</>
</Flex>
);
};
export default React.memo(function FlowEdit(props: Props) {
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
const filterAppIds = useMemo(() => [appDetail._id], [appDetail._id]);
const Render = () => {
return (
<WorkflowContextProvider
value={{
appId: appDetail._id,
mode: 'app',
filterAppIds,
basicNodeTemplates: appSystemModuleTemplates
}}
>
<Render {...props} />
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
<WorkflowEdit />
</WorkflowContextProvider>
);
});
};
export default React.memo(Render);

View File

@@ -0,0 +1,222 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, HStack, useDisclosure } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import Avatar from '@/components/Avatar';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import { WorkflowContext } from './context';
import { compareWorkflow, 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';
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
const AppCard = ({ showSaveStatus }: { showSaveStatus: boolean }) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { copyData } = useCopyData();
const { feConfigs } = useSystemStore();
const { appDetail, appLatestVersion, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
useContextSelector(AppContext, (v) => v);
const { historiesDefaultData, flowData2StoreDataAndCheck, onSaveWorkflow, isSaving, saveLabel } =
useContextSelector(WorkflowContext, (v) => v);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const onExportWorkflow = useCallback(async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
chatConfig: appDetail.chatConfig
},
null,
2
),
appT('Export Config Successful')
);
}
}, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
const isPublished = (() => {
const data = flowData2StoreDataAndCheck(true);
if (!appLatestVersion) return true;
if (data) {
return compareWorkflow(
{
nodes: appLatestVersion.nodes,
edges: appLatestVersion.edges,
chatConfig: appLatestVersion.chatConfig
},
{
nodes: data.nodes,
edges: data.edges,
chatConfig: appDetail.chatConfig
}
);
}
return false;
})();
const InfoMenu = useCallback(
({ children }: { children: React.ReactNode }) => {
return (
<MyMenu
width={150}
Button={children}
menuList={[
{
children: [
{
icon: 'edit',
label: appT('Edit info'),
onClick: onOpenInfoEdit
},
{
icon: 'support/team/key',
label: t('common.Role'),
onClick: onOpenInfoEdit
}
]
},
...(!historiesDefaultData && currentTab === TabEnum.appEdit
? [
{
children: [
{
label: appT('Import Configs'),
icon: 'common/importLight',
onClick: onOpenImport
},
{
label: appT('Export Configs'),
icon: 'export',
onClick: onExportWorkflow
}
]
}
]
: []),
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
? [
{
children: [
{
icon: 'support/team/memberLight',
label: t('common.Team Tags Set'),
onClick: onOpenTeamTagModal
}
]
}
]
: []),
...(appDetail.permission.isOwner
? [
{
children: [
{
type: 'danger' as 'danger',
icon: 'delete',
label: t('common.Delete'),
onClick: onDelApp
}
]
}
]
: [])
]}
/>
);
},
[
appDetail.permission.hasWritePer,
appDetail.permission.isOwner,
appT,
currentTab,
feConfigs?.show_team_chat,
historiesDefaultData,
onDelApp,
onExportWorkflow,
onOpenImport,
onOpenInfoEdit,
onOpenTeamTagModal,
t
]
);
const Render = useMemo(() => {
return (
<HStack>
<InfoMenu>
<Avatar src={appDetail.avatar} w={'1.75rem'} />
</InfoMenu>
<Box>
<InfoMenu>
<HStack spacing={1} cursor={'pointer'}>
<Box color={'myGray.900'}>{appDetail.name}</Box>
<MyIcon name={'common/select'} w={'1rem'} />
</HStack>
</InfoMenu>
{showSaveStatus && (
<MyTooltip label={t('core.app.Onclick to save')}>
<Flex
alignItems={'center'}
h={'20px'}
cursor={'pointer'}
fontSize={'mini'}
onClick={onSaveWorkflow}
lineHeight={1}
>
{isSaving && <MyIcon name={'common/loading'} w={'0.8rem'} mr={0.5} />}
<Box color={'myGray.500'}>{saveLabel}</Box>
<MyTag
py={0}
showDot
bg={'transparent'}
colorSchema={
isPublished
? publishStatusStyle.published.colorSchema
: publishStatusStyle.unPublish.colorSchema
}
>
{isPublished
? publishStatusStyle.published.text
: publishStatusStyle.unPublish.text}
</MyTag>
</Flex>
</MyTooltip>
)}
</Box>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
</HStack>
);
}, [
InfoMenu,
appDetail.avatar,
appDetail.name,
isOpenImport,
isPublished,
isSaving,
onCloseImport,
onSaveWorkflow,
saveLabel,
showSaveStatus,
t
]);
return Render;
};
export default AppCard;

View File

@@ -1,36 +1,17 @@
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import React, {
useMemo,
useCallback,
useRef,
forwardRef,
useImperativeHandle,
ForwardedRef
} from 'react';
import React, { useRef, forwardRef, ForwardedRef, useImperativeHandle } from 'react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { streamFetch } from '@/web/common/api/fetch';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import ChatBox from '@/components/ChatBox';
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
import {
checkChatSupportSelectFileByModules,
getAppQuestionGuidesByModules
} from '@/web/core/chat/utils';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { ComponentRef } from '@/components/ChatBox/type.d';
import { useTranslation } from 'next-i18next';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
getDefaultEntryNodeIds,
getMaxHistoryLimitFromNodes,
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
import { useChatTest } from '@/pages/app/detail/components/useChatTest';
export type ChatTestComponentRef = {
resetChatTest: () => void;
@@ -52,48 +33,32 @@ const ChatTest = (
) => {
const { t } = useTranslation();
const ChatBoxRef = useRef<ComponentRef>(null);
const { userInfo } = useUserStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const startChat = useCallback(
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
/* get histories */
let historyMaxLen = getMaxHistoryLimitFromNodes(nodes);
const history = chatList.slice(-historyMaxLen - 2, -2);
// 流请求,获取数据
const { responseText, responseData } = await streamFetch({
url: '/api/core/chat/chatTest',
data: {
history,
prompt: chatList[chatList.length - 2].value,
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
edges: initWorkflowEdgeStatus(edges),
variables,
appId: appDetail._id,
appName: `调试-${appDetail.name}`,
mode: 'test'
},
onMessage: generatingMessage,
abortCtrl: controller
});
return { responseText, responseData };
},
[appDetail._id, appDetail.name, edges, nodes]
);
const { resetChatBox, ChatBox } = useChatTest({
nodes,
edges,
chatConfig: appDetail.chatConfig
});
useImperativeHandle(ref, () => ({
resetChatTest() {
ChatBoxRef.current?.resetHistory([]);
ChatBoxRef.current?.resetVariables();
}
resetChatTest: resetChatBox
}));
return (
<>
<Box
zIndex={300}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
top={0}
left={0}
bottom={0}
right={0}
onClick={onClose}
/>
<Flex
zIndex={101}
zIndex={300}
flexDirection={'column'}
position={'absolute'}
top={5}
@@ -127,7 +92,7 @@ const ChatTest = (
</MyTooltip>
<MyTooltip label={t('common.Close')}>
<IconButton
ml={[3, 6]}
ml={3}
icon={<SmallCloseIcon fontSize={'22px'} />}
variant={'grayBase'}
size={'smSquare'}
@@ -137,29 +102,9 @@ const ChatTest = (
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatBox
ref={ChatBoxRef}
appId={appDetail._id}
appAvatar={appDetail.avatar}
userAvatar={userInfo?.avatar}
showMarkIcon
chatConfig={appDetail.chatConfig}
showFileSelector={checkChatSupportSelectFileByModules(nodes)}
onStartChat={startChat}
onDelMessage={() => {}}
/>
<ChatBox />
</Box>
</Flex>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
top={0}
left={0}
bottom={0}
right={0}
onClick={onClose}
/>
</>
);
};

View File

@@ -11,24 +11,24 @@ import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getPreviewPluginModule } from '@/web/core/plugin/api';
import { getPreviewPluginNode, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { workflowNodeTemplateList } from '@fastgpt/web/core/workflow/constants';
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import ParentPaths from '@/components/common/ParentPaths';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { useQuery } from '@tanstack/react-query';
import { debounce } from 'lodash';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useCreation } from 'ahooks';
import { useI18n } from '@/web/context/I18n';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import FolderPath from '@/components/common/folder/Path';
import { getAppFolderPath } from '@/web/core/app/api/app';
type ModuleTemplateListProps = {
isOpen: boolean;
@@ -37,8 +37,8 @@ type ModuleTemplateListProps = {
type RenderListProps = {
templates: FlowNodeTemplateType[];
onClose: () => void;
currentParent?: { parentId: string; parentName: string };
setCurrentParent: (e: { parentId: string; parentName: string }) => void;
parentId: ParentIdType;
setParentId: React.Dispatch<React.SetStateAction<ParentIdType>>;
};
enum TemplateTypeEnum {
@@ -52,195 +52,175 @@ const sliderWidth = 390;
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const { t } = useTranslation();
const router = useRouter();
const [currentParent, setCurrentParent] = useState<RenderListProps['currentParent']>();
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore();
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { basicNodeTemplates, hasToolNode, nodeList } = useContextSelector(
WorkflowContext,
(v) => v
);
const {
systemNodeTemplates,
loadSystemNodeTemplates,
teamPluginNodeTemplates,
loadTeamPluginNodeTemplates
} = useWorkflowStore();
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
const templates = useCreation(() => {
const map = {
[TemplateTypeEnum.basic]: basicNodeTemplates.filter((item) => {
// unique node filter
if (item.unique) {
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
if (nodeExist) {
const { data: templates = [], loading } = useRequest2(
async () => {
if (templateType === TemplateTypeEnum.basic) {
return basicNodeTemplates.filter((item) => {
// unique node filter
if (item.unique) {
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
if (nodeExist) {
return false;
}
}
// special node filter
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
return false;
}
}
// special node filter
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
return false;
}
// tool stop
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
return false;
}
return true;
}),
[TemplateTypeEnum.systemPlugin]: systemNodeTemplates,
[TemplateTypeEnum.teamPlugin]: teamPluginNodeTemplates.filter((item) =>
searchKey ? item.pluginType !== PluginTypeEnum.folder : true
)
};
return map[templateType];
}, [
basicNodeTemplates,
feConfigs.lafEnv,
hasToolNode,
nodeList,
searchKey,
systemNodeTemplates,
teamPluginNodeTemplates,
templateType
]);
const { mutate: onChangeTab } = useRequest({
mutationFn: async (e: any) => {
const val = e as TemplateTypeEnum;
if (val === TemplateTypeEnum.systemPlugin) {
await loadSystemNodeTemplates();
} else if (val === TemplateTypeEnum.teamPlugin) {
await loadTeamPluginNodeTemplates({
parentId: currentParent?.parentId
// tool stop
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
return false;
}
return true;
});
}
setTemplateType(val);
if (templateType === TemplateTypeEnum.systemPlugin) {
return getSystemPlugTemplates();
}
if (templateType === TemplateTypeEnum.teamPlugin) {
return getTeamPlugTemplates({
parentId,
searchKey,
type: [AppTypeEnum.folder, AppTypeEnum.httpPlugin, AppTypeEnum.plugin]
});
}
return [];
},
errorToast: t('core.module.templates.Load plugin error')
{
manual: false,
throttleWait: 300,
refreshDeps: [basicNodeTemplates, nodeList, hasToolNode, templateType, searchKey, parentId]
}
);
const { data: paths = [] } = useRequest2(() => getAppFolderPath(parentId), {
manual: false,
refreshDeps: [parentId]
});
useQuery(['teamNodeTemplate', currentParent?.parentId, searchKey], () =>
loadTeamPluginNodeTemplates({
parentId: currentParent?.parentId,
searchKey,
init: true
})
);
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
onClick={onClose}
fontSize={'sm'}
/>
<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'}
>
<Box mb={2} pl={'20px'} pr={'10px'} whiteSpace={'nowrap'} overflow={'hidden'}>
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<RowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('core.module.template.Team Plugin'),
value: TemplateTypeEnum.teamPlugin
}
]}
py={'5px'}
value={templateType}
onChange={onChangeTab}
/>
{/* 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>
{templateType === TemplateTypeEnum.teamPlugin && (
<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={t('plugin.Search plugin')}
onChange={debounce((e) => setSearchKey(e.target.value), 200)}
/>
</InputGroup>
<Box flex={1} />
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/plugin/list')}
>
<Box></Box>
<MyIcon name={'common/rightArrowLight'} w={'14px'} />
</Flex>
</Flex>
)}
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && currentParent && (
<Flex alignItems={'center'} mt={2}>
<ParentPaths
paths={[currentParent]}
FirstPathDom={null}
onClick={() => {
setCurrentParent(undefined);
}}
fontSize="md"
const Render = useMemo(() => {
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
onClick={onClose}
fontSize={'sm'}
/>
<MyBox
isLoading={loading}
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'}
>
<Box pl={'20px'} mb={3} pr={'10px'} whiteSpace={'nowrap'} overflow={'hidden'}>
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<RowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('core.module.template.Team Plugin'),
value: TemplateTypeEnum.teamPlugin
}
]}
py={'5px'}
value={templateType}
onChange={(e) => setTemplateType(e as TemplateTypeEnum)}
/>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
borderColor={'myGray.300'}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
/>
</Flex>
)}
</Box>
<RenderList
templates={templates}
onClose={onClose}
currentParent={currentParent}
setCurrentParent={setCurrentParent}
/>
</Flex>
</>
);
{templateType === TemplateTypeEnum.teamPlugin && (
<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={t('plugin.Search plugin')}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
<Box flex={1} />
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/app/list')}
>
<Box></Box>
<MyIcon name={'common/rightArrowLight'} w={'14px'} />
</Flex>
</Flex>
)}
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && parentId && (
<Flex alignItems={'center'} mt={2}>
<FolderPath paths={paths} FirstPathDom={null} onClick={setParentId} />
</Flex>
)}
</Box>
<RenderList
templates={templates}
onClose={onClose}
parentId={parentId}
setParentId={setParentId}
/>
</MyBox>
</>
);
}, [isOpen, onClose, loading, t, templateType, searchKey, parentId, paths, templates, router]);
return Render;
};
export default React.memo(NodeTemplatesModal);
@@ -248,8 +228,8 @@ export default React.memo(NodeTemplatesModal);
const RenderList = React.memo(function RenderList({
templates,
onClose,
currentParent,
setCurrentParent
parentId,
setParentId
}: RenderListProps) {
const { t } = useTranslation();
const { appT } = useI18n();
@@ -270,7 +250,7 @@ const RenderList = React.memo(function RenderList({
});
return copy.filter((item) => item.list.length > 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [templates, currentParent]);
}, [templates, parentId]);
const onAddNode = useCallback(
async ({ template, position }: { template: FlowNodeTemplateType; position: XYPosition }) => {
@@ -281,7 +261,7 @@ const RenderList = React.memo(function RenderList({
// get plugin preview module
if (template.flowNodeType === FlowNodeTypeEnum.pluginModule) {
setLoading(true);
const res = await getPreviewPluginModule(template.id);
const res = await getPreviewPluginNode({ appId: template.id });
setLoading(false);
return res;
@@ -375,7 +355,7 @@ const RenderList = React.memo(function RenderList({
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={template.pluginType !== PluginTypeEnum.folder}
draggable={template.pluginType !== AppTypeEnum.folder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
@@ -384,11 +364,11 @@ const RenderList = React.memo(function RenderList({
});
}}
onClick={(e) => {
if (template.pluginType === PluginTypeEnum.folder) {
return setCurrentParent({
parentId: template.id,
parentName: template.name
});
if (
template.pluginType === AppTypeEnum.folder ||
template.pluginType === AppTypeEnum.httpPlugin
) {
return setParentId(template.id);
}
if (isPc) {
return onAddNode({
@@ -421,7 +401,7 @@ const RenderList = React.memo(function RenderList({
</Box>
</Box>
);
}, [appT, formatTemplates, isPc, onAddNode, onClose, setCurrentParent, t, templates.length]);
}, [appT, formatTemplates, isPc, onAddNode, onClose, setParentId, t, templates.length]);
return Render;
});

View File

@@ -93,7 +93,7 @@ const ButtonEdge = (props: EdgeProps) => {
if (highlightEdge) return '#3370ff';
return '#94B5FF';
}
console.log(targetEdge);
// debug mode
const colorMap = {
[RuntimeEdgeStatusEnum.active]: '#39CC83',

View File

@@ -272,3 +272,7 @@ export const useDebug = () => {
openDebugNode
};
};
export default function Dom() {
return <></>;
}

View File

@@ -9,7 +9,7 @@ import { WorkflowContext, getWorkflowStore } from '../../context';
export const useKeyboard = () => {
const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
const { setNodes, onSaveWorkflow } = useContextSelector(WorkflowContext, (v) => v);
const { copyData } = useCopyData();
const [isDowningCtrl, setIsDowningCtrl] = useState(false);
@@ -81,6 +81,7 @@ export const useKeyboard = () => {
(event: KeyboardEvent) => {
if (event.ctrlKey || event.metaKey) {
setIsDowningCtrl(true);
switch (event.key) {
case 'c':
onCopy();
@@ -88,12 +89,17 @@ export const useKeyboard = () => {
case 'v':
onParse();
break;
case 's':
event.preventDefault();
onSaveWorkflow();
break;
default:
break;
}
}
},
[onCopy, onParse]
[onCopy, onParse, onSaveWorkflow]
);
const handleKeyUp = useCallback((event: KeyboardEvent) => {
@@ -118,3 +124,7 @@ export const useKeyboard = () => {
isDowningCtrl
};
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,127 @@
import React, { useCallback, useMemo } from 'react';
import { Connection, NodeChange, OnConnectStartParams, addEdge, EdgeChange, Edge } from 'reactflow';
import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
import 'reactflow/dist/style.css';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useKeyboard } from './useKeyboard';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
export const useWorkflow = () => {
const { toast } = useToast();
const { t } = useTranslation();
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
content: t('core.module.Confirm Delete Node'),
type: 'delete'
});
const { isDowningCtrl } = useKeyboard();
const { setConnectingEdge, nodes, onNodesChange, setEdges, onEdgesChange, setHoverEdgeId } =
useContextSelector(WorkflowContext, (v) => v);
/* node */
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
for (const change of changes) {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
if (node && node.data.forbidDelete) {
return toast({
status: 'warning',
title: t('core.workflow.Can not delete node')
});
} else {
return onOpenConfirmDeleteNode(() => {
onNodesChange(changes);
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
})();
}
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
change.selected = true;
}
}
onNodesChange(changes);
},
[isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast]
);
const handleEdgeChange = useCallback(
(changes: EdgeChange[]) => {
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
},
[onEdgesChange]
);
/* connect */
const onConnectStart = useCallback(
(event: any, params: OnConnectStartParams) => {
setConnectingEdge(params);
},
[setConnectingEdge]
);
const onConnectEnd = useCallback(() => {
setConnectingEdge(undefined);
}, [setConnectingEdge]);
const onConnect = useCallback(
({ connect }: { connect: Connection }) => {
setEdges((state) =>
addEdge(
{
...connect,
type: EDGE_TYPE
},
state
)
);
},
[setEdges]
);
const customOnConnect = useCallback(
(connect: Connection) => {
if (!connect.sourceHandle || !connect.targetHandle) {
return;
}
if (connect.source === connect.target) {
return toast({
status: 'warning',
title: t('core.module.Can not connect self')
});
}
onConnect({
connect
});
},
[onConnect, t, toast]
);
/* edge */
const onEdgeMouseEnter = useCallback(
(e: any, edge: Edge) => {
setHoverEdgeId(edge.id);
},
[setHoverEdgeId]
);
const onEdgeMouseLeave = useCallback(() => {
setHoverEdgeId(undefined);
}, [setHoverEdgeId]);
return {
ConfirmDeleteModal,
handleNodesChange,
handleEdgeChange,
onConnectStart,
onConnectEnd,
onConnect,
customOnConnect,
onEdgeMouseEnter,
onEdgeMouseLeave
};
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,184 @@
import React, { useMemo } from 'react';
import ReactFlow, {
Background,
Controls,
ControlButton,
MiniMap,
NodeProps,
ReactFlowProvider,
useReactFlow
} 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';
import dynamic from 'next/dynamic';
import ButtonEdge from './components/ButtonEdge';
import NodeTemplatesModal from './NodeTemplatesModal';
import 'reactflow/dist/style.css';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useWorkflow } from './hooks/useWorkflow';
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.emptyNode]: NodeSimple,
[FlowNodeTypeEnum.globalVariable]: NodeSimple,
[FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')),
[FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')),
[FlowNodeTypeEnum.chatNode]: NodeSimple,
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(() => import('./nodes/NodeDatasetConcat')),
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./nodes/NodeAnswer')),
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./nodes/NodeCQNode')),
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./nodes/NodeExtract')),
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./nodes/NodeHttp')),
[FlowNodeTypeEnum.runApp]: NodeSimple,
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
[FlowNodeTypeEnum.tools]: dynamic(() => import('./nodes/NodeTools')),
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowNodeItemType>) => (
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
),
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
};
const Workflow = () => {
const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v);
const {
ConfirmDeleteModal,
handleNodesChange,
handleEdgeChange,
onConnectStart,
onConnectEnd,
customOnConnect,
onEdgeMouseEnter,
onEdgeMouseLeave
} = useWorkflow();
const {
isOpen: isOpenTemplate,
onOpen: onOpenTemplate,
onClose: onCloseTemplate
} = useDisclosure();
return (
<ReactFlowProvider>
<Box
flex={'1 0 0'}
h={0}
w={'100%'}
position={'relative'}
onContextMenu={(e) => {
e.preventDefault();
return false;
}}
>
{/* open module template */}
<>
<IconButton
position={'absolute'}
top={5}
left={5}
size={'mdSquare'}
borderRadius={'50%'}
icon={<SmallCloseIcon fontSize={'26px'} />}
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
transition={'0.2s ease'}
aria-label={''}
zIndex={1}
boxShadow={'2px 2px 6px #85b1ff'}
onClick={() => {
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
}}
/>
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</>
<ReactFlow
ref={reactFlowWrapper}
fitView
nodes={nodes}
edges={edges}
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={defaultEdgeOptions}
elevateEdgesOnSelect
connectionLineStyle={connectionLineStyle}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgeChange}
onConnect={customOnConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
>
<FlowController />
</ReactFlow>
</Box>
<ConfirmDeleteModal />
</ReactFlowProvider>
);
};
export default React.memo(Workflow);
const FlowController = React.memo(function FlowController() {
const { fitView } = useReactFlow();
const Render = useMemo(() => {
return (
<>
<MiniMap
style={{
height: 78,
width: 126,
marginBottom: 35
}}
pannable
/>
<Controls
position={'bottom-right'}
style={{
display: 'flex',
marginBottom: 5,
background: 'white',
borderRadius: '6px',
overflow: 'hidden',
boxShadow:
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
}}
showInteractive={false}
showFitView={false}
>
<MyTooltip label={'页面居中'}>
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
<MyIcon name={'core/modules/fixview'} w={'14px'} />
</ControlButton>
</MyTooltip>
</Controls>
<Background />
</>
);
}, [fitView]);
return Render;
});

View File

@@ -38,7 +38,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useMemoizedFn } from 'ahooks';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));

View File

@@ -30,7 +30,7 @@ import { SourceHandle } from '../render/Handle';
import { Position, useReactFlow } from 'reactflow';
import { getRefData } from '@/web/core/workflow/utils';
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
import { useI18n } from '@/web/context/I18n';
const ListItem = ({

View File

@@ -8,8 +8,8 @@ import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/
import { useTranslation } from 'next-i18next';
import { getLafAppDetail } from '@/web/support/laf/api';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { getApiSchemaByUrl } from '@/web/core/plugin/api';
import { getType, str2OpenApiSchema } from '@fastgpt/global/core/plugin/httpPlugin/utils';
import { getApiSchemaByUrl } from '@/web/core/app/api/plugin';
import { getType, str2OpenApiSchema } from '@fastgpt/global/core/app/httpPlugin/utils';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ChevronRightIcon } from '@chakra-ui/icons';

View File

@@ -23,7 +23,7 @@ import {
FlowNodeOutputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowValueTypeMap } from '@/web/core/workflow/constants/dataType';
import VariableTable from '../nodes/render/VariableTable';
import VariableTable from './render/VariableTable';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';

View File

@@ -17,7 +17,7 @@ import { WorkflowContext } from '../../context';
import { AppChatConfigType, AppDetailType, VariableItemType } from '@fastgpt/global/core/app/type';
import { useMemoizedFn } from 'ahooks';
import VariableEdit from '@/components/core/app/VariableEdit';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
import WelcomeTextConfig from '@/components/core/app/WelcomeTextConfig';
type ComponentProps = {

View File

@@ -18,7 +18,7 @@ import {
import { TUpdateListItem } from '@fastgpt/global/core/workflow/template/system/variableUpdate/type';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { WorkflowContext } from '../../context';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum
@@ -32,7 +32,7 @@ import { ReferenceValueProps } 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 '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data;

View File

@@ -13,7 +13,7 @@ import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { AppContext } from '@/web/core/app/context/appContext';
import { AppContext } from '@/pages/app/detail/components/context';
const NodeStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();

View File

@@ -4,7 +4,7 @@ import { SourceHandle, TargetHandle } from '.';
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 '@/components/core/workflow/context';
import { WorkflowContext } from '../../../../context';
export const ConnectionSourceHandle = ({ nodeId }: { nodeId: string }) => {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge);
@@ -183,4 +183,6 @@ export const ConnectionTargetHandle = ({ nodeId }: { nodeId: string }) => {
) : null;
};
export default <></>;
export default function Dom() {
return <></>;
}

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