Concat plugin to app (#1799)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -91,7 +91,7 @@ const MyRadio = ({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Radio isChecked={value === item.value} />
|
||||
{!hiddenCircle && <Radio isChecked={value === item.value} />}
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
2
projects/app/src/global/core/app/api.d.ts
vendored
2
projects/app/src/global/core/app/api.d.ts
vendored
@@ -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'];
|
||||
};
|
||||
|
||||
@@ -6,8 +6,7 @@ export type PostWorkflowDebugProps = {
|
||||
nodes: RuntimeNodeItemType[];
|
||||
edges: RuntimeEdgeItemType[];
|
||||
variables: Record<string, any>;
|
||||
appId?: string;
|
||||
pluginId?: string;
|
||||
appId: string;
|
||||
};
|
||||
|
||||
export type PostWorkflowDebugResponse = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
163
projects/app/src/pages/api/admin/initv485.ts
Normal file
163
projects/app/src/pages/api/admin/initv485.ts
Normal 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);
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
68
projects/app/src/pages/api/core/app/httpPlugin/create.ts
Normal file
68
projects/app/src/pages/api/core/app/httpPlugin/create.ts
Normal 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);
|
||||
127
projects/app/src/pages/api/core/app/httpPlugin/update.ts
Normal file
127
projects/app/src/pages/api/core/app/httpPlugin/update.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
33
projects/app/src/pages/api/core/app/plugin/getPreviewNode.ts
Normal file
33
projects/app/src/pages/api/core/app/plugin/getPreviewNode.ts
Normal 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);
|
||||
@@ -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: []
|
||||
})) || [];
|
||||
53
projects/app/src/pages/api/core/app/transitionWorkflow.ts
Normal file
53
projects/app/src/pages/api/core/app/transitionWorkflow.ts
Normal 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);
|
||||
@@ -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 && {
|
||||
|
||||
36
projects/app/src/pages/api/core/app/version/latest.ts
Normal file
36
projects/app/src/pages/api/core/app/version/latest.ts
Normal 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);
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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 })
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
191
projects/app/src/pages/app/detail/components/Plugin/Header.tsx
Normal file
191
projects/app/src/pages/app/detail/components/Plugin/Header.tsx
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
75
projects/app/src/pages/app/detail/components/RouteTab.tsx
Normal file
75
projects/app/src/pages/app/detail/components/RouteTab.tsx
Normal 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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
) : (
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
198
projects/app/src/pages/app/detail/components/Workflow/Header.tsx
Normal file
198
projects/app/src/pages/app/detail/components/Workflow/Header.tsx
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
@@ -93,7 +93,7 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
if (highlightEdge) return '#3370ff';
|
||||
return '#94B5FF';
|
||||
}
|
||||
console.log(targetEdge);
|
||||
|
||||
// debug mode
|
||||
const colorMap = {
|
||||
[RuntimeEdgeStatusEnum.active]: '#39CC83',
|
||||
@@ -272,3 +272,7 @@ export const useDebug = () => {
|
||||
openDebugNode
|
||||
};
|
||||
};
|
||||
|
||||
export default function Dom() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -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 <></>;
|
||||
}
|
||||
@@ -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 <></>;
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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'));
|
||||
|
||||
@@ -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 = ({
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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
Reference in New Issue
Block a user