Concat plugin to app (#1799)

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

View File

@@ -25,7 +25,7 @@ WORKDIR /app
ARG proxy
# copy common node_modules and one project node_modules
COPY package.json pnpm-workspace.yaml .npmrc ./
COPY package.json pnpm-workspace.yaml .npmrc tsconfig.json ./
COPY --from=mainDeps /app/node_modules ./node_modules
COPY --from=mainDeps /app/packages ./packages
COPY ./projects/app ./projects/app

View File

@@ -1,5 +1,6 @@
{
"author": "FastGPT Team",
"version": "481",
"templateType": "other",
"name": "自定义反馈",
"avatar": "/imgs/workflow/customFeedback.svg",

View File

@@ -1,5 +1,6 @@
{
"author": "FastGPT Team",
"version": "481",
"templateType": "tools",
"name": "获取当前时间",
"avatar": "/imgs/workflow/getCurrentTime.svg",

View File

@@ -1,5 +1,6 @@
{
"author": "FastGPT Team",
"version": "481",
"templateType": "tools",
"name": "文本加工",
"avatar": "/imgs/workflow/textEditor.svg",

View File

@@ -15,8 +15,10 @@
"Copy Module Config": "Copy Config",
"Create bot": "App",
"Create one ai app": "Create AI app",
"Current settings": "Current settings",
"Dataset Quote Template": "Knowledge Base QA Mode",
"Edit app": "Edit app",
"Edit info": "Edit info",
"Export Config Successful": "Config copied, please check for important data",
"Export Configs": "Export Configs",
"Feedback Count": "User Feedback",
@@ -34,8 +36,15 @@
"My Apps": "My Apps",
"Output Field Settings": "Output Field Settings",
"Paste Config": "Paste Config",
"Publish channel": "Publish channel",
"Publish success": "Publish success",
"Setting app": "Settings",
"Setting plugin": "Setting plugin",
"To Chat": "Go to Chat",
"To Settings": "View Details",
"Transition to workflow": "Transition to workflow",
"Transition to workflow create new placeholder": "Create a new application instead of modifying the current one",
"Transition to workflow create new tip": "After converting to workflow, it will not be able to convert back to simple mode, please confirm!",
"Variable Key Repeat Tip": "Variable key is duplicate",
"app": {
"modules": {
@@ -48,7 +57,7 @@
"Confirm Sync": "Using the latest template will overwrite the existing one and may result in the loss of some previous configuration information. Please confirm.",
"Custom Title Tip": "This title will be displayed during the conversation",
"My Modules": "My Modules",
"No Modules": "No modules yet~",
"No Modules": "No plugins yet~",
"System Module": "System Module",
"type": "\"{{type}}\" type\n{{description}}"
},
@@ -56,6 +65,16 @@
"Title is required": "Module name cannot be empty"
},
"type": {
"All": "All",
"Create http plugin tip": "Create plug-ins in batches using OpenAPI schema, compatible with GPTs format.",
"Create one plugin tip": "The input and output workflows can be customized",
"Create plugin bot": "Create plugin bot",
"Create simple bot": "Create simple bot",
"Create simple bot tip": "Create simple AI applications in form form",
"Create workflow bot": "Create workflow bot",
"Create workflow tip": "Through the way of low code, build a logically complex multi-round dialogue AI application, recommended for advanced players",
"Http plugin": "Http plugin",
"Plugin": "Plugin",
"Simple bot": "Simple bot",
"Workflow bot": "Workflow"
}

View File

@@ -122,6 +122,7 @@
"Status": "Status",
"Submit failed": "Submit failed",
"Submit success": "Submit success",
"Success": "Success",
"Sync success": "Sync success",
"System Output": "System Output",
"System version": "System version",
@@ -256,6 +257,7 @@
"Quote templates": "Quote content templates",
"Random": "Diverge",
"Save and preview": "Save and preview",
"Saved time": "Saved: {{time}}",
"Search team tags": "Search tags",
"Select TTS": "Select voice playback mode",
"Select app from template": "Select from template",
@@ -285,7 +287,6 @@
"Whisper": "Voice input",
"Whisper Tip": "Configure voice input related parameters",
"Whisper config": "Voice input configuration",
"create app": "Create your own AI app",
"deterministic": "Rigorous",
"edit": {
"Confirm Save App Tip": "This app may be in advanced arrangement mode, saving will overwrite the advanced arrangement configuration, please confirm!",

View File

@@ -14,8 +14,10 @@
"Copy Module Config": "复制配置",
"Create bot": "应用",
"Create one ai app": "创建一个AI应用",
"Current settings": "当前配置",
"Dataset Quote Template": "知识库问答模式",
"Edit app": "编辑应用",
"Edit info": "编辑信息",
"Export Config Successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据",
"Export Configs": "导出配置",
"Feedback Count": "用户反馈",
@@ -33,8 +35,15 @@
"My Apps": "我的应用",
"Output Field Settings": "输出字段编辑",
"Paste Config": "粘贴配置",
"Publish channel": "发布渠道",
"Publish success": "发布成功",
"Setting app": "应用配置",
"Setting plugin": "插件配置",
"To Chat": "前去对话",
"To Settings": "查看详情",
"Transition to workflow": "转成工作流",
"Transition to workflow create new placeholder": "创建一个新的应用,而不是修改当前应用",
"Transition to workflow create new tip": "转化成工作流后,将无法转化回简易模式,请确认!",
"Variable Key Repeat Tip": "变量 key 重复",
"app": {
"modules": {
@@ -47,7 +56,7 @@
"Confirm Sync": "将会使用最新模板进行覆盖,可能会丢失一些旧的配置信息,请确认",
"Custom Title Tip": "该标题名字会展示在对话过程中",
"My Modules": "",
"No Modules": "还没有模块~",
"No Modules": "没找到插件",
"System Module": "系统模块",
"type": "\"{{type}}\"类型\n{{description}}"
},
@@ -55,6 +64,16 @@
"Title is required": "模块名不能为空"
},
"type": {
"All": "全部",
"Create http plugin tip": "通过 OpenAPI schema 批量创建插件,兼容 GPTs 格式。",
"Create one plugin tip": "可以自定义输入和输出的工作流,通常用于封装重复使用的工作流",
"Create plugin bot": "创建插件",
"Create simple bot": "创建简易应用",
"Create simple bot tip": "通过填表单形式创建简单的AI应用适合新手",
"Create workflow bot": "创建工作流",
"Create workflow tip": "通过低代码的方式构建逻辑复杂的多轮对话AI应用推荐高级玩家使用",
"Http plugin": "HTTP 插件",
"Plugin": "插件",
"Simple bot": "简易应用",
"Workflow bot": "工作流"
}

View File

@@ -123,6 +123,7 @@
"Status": "状态",
"Submit failed": "提交失败",
"Submit success": "提交成功",
"Success": "成功",
"Sync success": "同步成功",
"System Output": "系统输出",
"System version": "系统版本",
@@ -257,6 +258,7 @@
"Quote templates": "引用内容模板",
"Random": "发散",
"Save and preview": "保存并预览",
"Saved time": "已保存: {{time}}",
"Search team tags": "搜索标签",
"Select TTS": "选择语音播放模式",
"Select app from template": "从模板中选择",
@@ -286,7 +288,6 @@
"Whisper": "语音输入",
"Whisper Tip": "配置语音输入相关参数",
"Whisper config": "语音输入配置",
"create app": "创建属于你的 AI 应用",
"deterministic": "严谨",
"edit": {
"Confirm Save App Tip": "该应用可能为高级编排模式,保存后将会覆盖高级编排配置,请确认!",

View File

@@ -0,0 +1,13 @@
<svg viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.75" width="32" height="32" rx="5.70173" fill="url(#paint0_linear_5533_28340)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M5.08823 18.1872C5.08823 21.6801 7.78709 24.5426 11.2132 24.8031V24.815H11.4056C11.5109 24.8199 11.6169 24.8225 11.7234 24.8225C11.83 24.8225 11.9359 24.8199 12.0413 24.815H20.7402C20.8393 24.8199 20.9389 24.8225 21.0392 24.8225C24.2826 24.8225 26.9118 22.1932 26.9118 18.9498C26.9118 16.5708 25.4972 14.5222 23.4633 13.5992C22.7505 10.7684 20.2467 8.67749 17.2673 8.67749C15.0662 8.67749 13.1245 9.81881 11.973 11.5566C11.8902 11.5536 11.807 11.552 11.7234 11.552C8.05891 11.552 5.08823 14.5227 5.08823 18.1872ZM8.27323 20.0908C8.35893 20.1765 8.48035 20.2194 8.63748 20.2194C8.80175 20.2194 8.92496 20.1765 9.00709 20.0908C9.08923 20.0016 9.1303 19.8766 9.1303 19.7159V18.6606H10.6891V19.7159C10.6891 19.8766 10.7302 20.0016 10.8123 20.0908C10.898 20.1765 11.0212 20.2194 11.1819 20.2194C11.339 20.2194 11.4587 20.1765 11.5408 20.0908C11.6265 20.0016 11.6694 19.8766 11.6694 19.7159V16.85C11.6694 16.6858 11.6265 16.5608 11.5408 16.4751C11.4587 16.3894 11.339 16.3465 11.1819 16.3465C11.0212 16.3465 10.898 16.3894 10.8123 16.4751C10.7302 16.5608 10.6891 16.6858 10.6891 16.85V17.8517H9.1303V16.85C9.1303 16.6858 9.08744 16.5608 9.00174 16.4751C8.9196 16.3894 8.79818 16.3465 8.63748 16.3465C8.48035 16.3465 8.35893 16.3894 8.27323 16.4751C8.18752 16.5608 8.14467 16.6858 8.14467 16.85V19.7159C8.14467 19.8766 8.18752 20.0016 8.27323 20.0908ZM13.8021 20.0908C13.8878 20.1765 14.0092 20.2194 14.1663 20.2194C14.3306 20.2194 14.4538 20.1765 14.5359 20.0908C14.6181 20.0016 14.6591 19.8784 14.6591 19.7212V17.1982H15.5001C15.6323 17.1982 15.7322 17.1643 15.8001 17.0964C15.8715 17.025 15.9072 16.925 15.9072 16.7965C15.9072 16.6643 15.8715 16.5643 15.8001 16.4965C15.7322 16.4286 15.6323 16.3947 15.5001 16.3947H12.8325C12.7004 16.3947 12.5986 16.4286 12.5272 16.4965C12.4593 16.5643 12.4254 16.6643 12.4254 16.7965C12.4254 16.925 12.4593 17.025 12.5272 17.0964C12.5986 17.1643 12.7004 17.1982 12.8325 17.1982H13.6735V19.7212C13.6735 19.8784 13.7163 20.0016 13.8021 20.0908ZM18.1154 20.2194C17.9583 20.2194 17.8369 20.1765 17.7512 20.0908C17.6654 20.0016 17.6226 19.8784 17.6226 19.7212V17.1982H16.7816C16.6495 17.1982 16.5477 17.1643 16.4763 17.0964C16.4084 17.025 16.3745 16.925 16.3745 16.7965C16.3745 16.6643 16.4084 16.5643 16.4763 16.4965C16.5477 16.4286 16.6495 16.3947 16.7816 16.3947H19.4492C19.5814 16.3947 19.6813 16.4286 19.7492 16.4965C19.8206 16.5643 19.8563 16.6643 19.8563 16.7965C19.8563 16.925 19.8206 17.025 19.7492 17.0964C19.6813 17.1643 19.5814 17.1982 19.4492 17.1982H18.6082V19.7212C18.6082 19.8784 18.5672 20.0016 18.485 20.0908C18.4029 20.1765 18.2797 20.2194 18.1154 20.2194ZM20.7378 20.0908C20.8235 20.1765 20.9449 20.2194 21.1021 20.2194C21.2663 20.2194 21.3895 20.1765 21.4717 20.0908C21.5538 20.0016 21.5949 19.8784 21.5949 19.7212V18.8695H22.4252C22.8537 18.8695 23.184 18.7606 23.4162 18.5427C23.6518 18.3213 23.7697 18.0178 23.7697 17.6321C23.7697 17.2464 23.6518 16.9447 23.4162 16.7268C23.184 16.5054 22.8537 16.3947 22.4252 16.3947H21.1074C20.9503 16.3947 20.8271 16.4376 20.7378 16.5233C20.6521 16.609 20.6092 16.7322 20.6092 16.8929V19.7212C20.6092 19.8784 20.6521 20.0016 20.7378 20.0908ZM22.2538 18.1142H21.5949V17.15H22.2538C22.4394 17.15 22.5823 17.1893 22.6823 17.2679C22.7823 17.3464 22.8323 17.4678 22.8323 17.6321C22.8323 17.7928 22.7823 17.9142 22.6823 17.9964C22.5823 18.0749 22.4394 18.1142 22.2538 18.1142Z"
fill="white" />
<defs>
<linearGradient id="paint0_linear_5533_28340" x1="16" y1="0.75" x2="4.88889" y2="30.0833"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FBA8E9" />
<stop offset="1" stop-color="#FF718A" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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