Concat plugin to app (#1799)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"author": "FastGPT Team",
|
||||
"version": "481",
|
||||
"templateType": "other",
|
||||
"name": "自定义反馈",
|
||||
"avatar": "/imgs/workflow/customFeedback.svg",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"author": "FastGPT Team",
|
||||
"version": "481",
|
||||
"templateType": "tools",
|
||||
"name": "获取当前时间",
|
||||
"avatar": "/imgs/workflow/getCurrentTime.svg",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"author": "FastGPT Team",
|
||||
"version": "481",
|
||||
"templateType": "tools",
|
||||
"name": "文本加工",
|
||||
"avatar": "/imgs/workflow/textEditor.svg",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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": "工作流"
|
||||
}
|
||||
|
||||
@@ -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": "该应用可能为高级编排模式,保存后将会覆盖高级编排配置,请确认!",
|
||||
|
||||
13
projects/app/public/imgs/app/httpPluginFill.svg
Normal file
13
projects/app/public/imgs/app/httpPluginFill.svg
Normal 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 |
@@ -29,10 +29,12 @@ export default function InputGuideBox({
|
||||
const { data = [] } = useRequest2(
|
||||
async () => {
|
||||
if (!text) return [];
|
||||
// More than 20 characters, it's basically meaningless
|
||||
if (text.length > 20) return [];
|
||||
return await queryChatInputGuideList(
|
||||
{
|
||||
appId,
|
||||
searchKey: text.slice(0, 50),
|
||||
searchKey: text,
|
||||
...outLinkAuthData
|
||||
},
|
||||
chatInputGuide.customUrl ? chatInputGuide.customUrl : undefined
|
||||
|
||||
@@ -79,7 +79,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
return (
|
||||
<>
|
||||
<Box h={'100%'} bg={'myGray.100'}>
|
||||
{isPc === true && (
|
||||
{isPc ? (
|
||||
<>
|
||||
{isHideNavbar ? (
|
||||
<Auth>{children}</Auth>
|
||||
@@ -94,8 +94,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isPc === false && (
|
||||
) : (
|
||||
<>
|
||||
<Box h={'100%'} display={['block', 'none']}>
|
||||
{phoneUnShowLayoutRoute[router.pathname] || isChatPage ? (
|
||||
|
||||
@@ -40,13 +40,6 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
link: `/app/list`,
|
||||
activeLink: ['/app/list', '/app/detail']
|
||||
},
|
||||
{
|
||||
label: t('navbar.Plugin'),
|
||||
icon: 'common/navbar/pluginLight',
|
||||
activeIcon: 'common/navbar/pluginFill',
|
||||
link: `/plugin/list`,
|
||||
activeLink: ['/plugin/list', '/plugin/edit']
|
||||
},
|
||||
{
|
||||
label: t('navbar.Datasets'),
|
||||
icon: 'core/dataset/datasetLight',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex, Grid, Image } from '@chakra-ui/react';
|
||||
import type { GridProps } from '@chakra-ui/react';
|
||||
import type { FlexProps, GridProps } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
@@ -9,10 +9,11 @@ interface Props extends GridProps {
|
||||
list: { id: string; icon?: string; label: string | React.ReactNode }[];
|
||||
activeId: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
inlineStyles?: FlexProps;
|
||||
onChange: (id: string) => void;
|
||||
}
|
||||
|
||||
const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
|
||||
const Tabs = ({ list, size = 'md', activeId, onChange, inlineStyles, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const sizeMap = useMemo(() => {
|
||||
switch (size) {
|
||||
@@ -55,6 +56,7 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
|
||||
borderBottom={'2px solid transparent'}
|
||||
px={3}
|
||||
whiteSpace={'nowrap'}
|
||||
{...inlineStyles}
|
||||
{...(activeId === item.id
|
||||
? {
|
||||
color: 'primary.600',
|
||||
|
||||
@@ -91,7 +91,7 @@ const MyRadio = ({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Radio isChecked={value === item.value} />
|
||||
{!hiddenCircle && <Radio isChecked={value === item.value} />}
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box, BoxProps, border } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
type Props = BoxProps & {
|
||||
list: {
|
||||
icon?: string;
|
||||
label: string | React.ReactNode;
|
||||
value: string;
|
||||
}[];
|
||||
value: string;
|
||||
onChange: (e: string) => void;
|
||||
};
|
||||
|
||||
const RowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => {
|
||||
return (
|
||||
<Box display={'inline-flex'} px={'3px'} {...props}>
|
||||
{list.map((item) => (
|
||||
<Flex
|
||||
key={item.value}
|
||||
flex={'1 0 0'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
px={px}
|
||||
py={py}
|
||||
userSelect={'none'}
|
||||
whiteSpace={'noWrap'}
|
||||
borderBottom={'2px solid'}
|
||||
{...(value === item.value
|
||||
? {
|
||||
bg: 'white',
|
||||
color: 'primary.600',
|
||||
borderColor: 'primary.600'
|
||||
}
|
||||
: {
|
||||
borderColor: 'myGray.100',
|
||||
onClick: () => onChange(item.value)
|
||||
})}
|
||||
>
|
||||
{item.icon && <MyIcon name={item.icon as any} mr={1} w={'14px'} />}
|
||||
<Box fontSize={'sm'}>{item.label}</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default RowTabs;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -9,9 +9,17 @@ const FolderPath = (props: {
|
||||
FirstPathDom?: React.ReactNode;
|
||||
onClick: (parentId: string) => void;
|
||||
fontSize?: string;
|
||||
hoverStyle?: BoxProps;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { paths, rootName = t('common.folder.Root Path'), FirstPathDom, onClick, fontSize } = props;
|
||||
const {
|
||||
paths,
|
||||
rootName = t('common.folder.Root Path'),
|
||||
FirstPathDom,
|
||||
onClick,
|
||||
fontSize,
|
||||
hoverStyle
|
||||
} = props;
|
||||
|
||||
const concatPaths = useMemo(
|
||||
() => [
|
||||
@@ -43,9 +51,10 @@ const FolderPath = (props: {
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
color: 'myGray.600',
|
||||
color: 'myGray.500',
|
||||
_hover: {
|
||||
bg: 'myGray.100'
|
||||
bg: 'myGray.100',
|
||||
...hoverStyle
|
||||
},
|
||||
onClick: () => {
|
||||
onClick(item.parentId);
|
||||
|
||||
@@ -13,10 +13,18 @@ const AppTypeTag = ({ type }: { type: AppTypeEnum }) => {
|
||||
label: appT('type.Simple bot'),
|
||||
icon: 'core/app/type/simple'
|
||||
},
|
||||
[AppTypeEnum.advanced]: {
|
||||
[AppTypeEnum.workflow]: {
|
||||
label: appT('type.Workflow bot'),
|
||||
icon: 'core/app/type/workflow'
|
||||
},
|
||||
[AppTypeEnum.plugin]: {
|
||||
label: appT('type.Plugin'),
|
||||
icon: 'core/app/type/plugin'
|
||||
},
|
||||
[AppTypeEnum.httpPlugin]: {
|
||||
label: appT('type.Http plugin'),
|
||||
icon: 'core/app/type/httpPlugin'
|
||||
},
|
||||
[AppTypeEnum.folder]: undefined
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Flex, TextareaProps } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import React, { useTransition } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import ChatFunctionTip from './Tip';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea';
|
||||
@@ -8,6 +8,7 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
const WelcomeTextConfig = (props: TextareaProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
@@ -27,4 +28,4 @@ const WelcomeTextConfig = (props: TextareaProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default WelcomeTextConfig;
|
||||
export default React.memo(WelcomeTextConfig);
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Connection,
|
||||
Controls,
|
||||
ControlButton,
|
||||
MiniMap,
|
||||
NodeProps,
|
||||
ReactFlowProvider,
|
||||
useReactFlow,
|
||||
NodeChange,
|
||||
OnConnectStartParams,
|
||||
addEdge,
|
||||
EdgeChange,
|
||||
Edge
|
||||
} from 'reactflow';
|
||||
import { Box, Flex, IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import ButtonEdge from './components/ButtonEdge';
|
||||
import NodeTemplatesModal from './NodeTemplatesModal';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useKeyboard } from './hooks/useKeyboard';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
|
||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
[FlowNodeTypeEnum.emptyNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.globalVariable]: NodeSimple,
|
||||
[FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')),
|
||||
[FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')),
|
||||
[FlowNodeTypeEnum.chatNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(() => import('./nodes/NodeDatasetConcat')),
|
||||
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./nodes/NodeAnswer')),
|
||||
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./nodes/NodeCQNode')),
|
||||
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./nodes/NodeExtract')),
|
||||
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./nodes/NodeHttp')),
|
||||
[FlowNodeTypeEnum.runApp]: NodeSimple,
|
||||
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginInput')),
|
||||
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginOutput')),
|
||||
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
|
||||
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
|
||||
[FlowNodeTypeEnum.tools]: dynamic(() => import('./nodes/NodeTools')),
|
||||
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowNodeItemType>) => (
|
||||
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
|
||||
),
|
||||
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
|
||||
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
|
||||
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
|
||||
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode'))
|
||||
};
|
||||
const edgeTypes = {
|
||||
[EDGE_TYPE]: ButtonEdge
|
||||
};
|
||||
|
||||
const Container = React.memo(function Container() {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
|
||||
content: t('core.module.Confirm Delete Node'),
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
const {
|
||||
setConnectingEdge,
|
||||
reactFlowWrapper,
|
||||
nodes,
|
||||
onNodesChange,
|
||||
edges,
|
||||
setEdges,
|
||||
onEdgesChange,
|
||||
setHoverEdgeId
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
/* node */
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
for (const change of changes) {
|
||||
if (change.type === 'remove') {
|
||||
const node = nodes.find((n) => n.id === change.id);
|
||||
if (node && node.data.forbidDelete) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.workflow.Can not delete node')
|
||||
});
|
||||
} else {
|
||||
return onOpenConfirmDeleteNode(() => {
|
||||
onNodesChange(changes);
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
|
||||
);
|
||||
})();
|
||||
}
|
||||
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
|
||||
change.selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
onNodesChange(changes);
|
||||
},
|
||||
[isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast]
|
||||
);
|
||||
const handleEdgeChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
|
||||
},
|
||||
[onEdgesChange]
|
||||
);
|
||||
|
||||
/* connect */
|
||||
const onConnectStart = useCallback(
|
||||
(event: any, params: OnConnectStartParams) => {
|
||||
setConnectingEdge(params);
|
||||
},
|
||||
[setConnectingEdge]
|
||||
);
|
||||
const onConnectEnd = useCallback(() => {
|
||||
setConnectingEdge(undefined);
|
||||
}, [setConnectingEdge]);
|
||||
const onConnect = useCallback(
|
||||
({ connect }: { connect: Connection }) => {
|
||||
setEdges((state) =>
|
||||
addEdge(
|
||||
{
|
||||
...connect,
|
||||
type: EDGE_TYPE
|
||||
},
|
||||
state
|
||||
)
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
const customOnConnect = useCallback(
|
||||
(connect: Connection) => {
|
||||
if (!connect.sourceHandle || !connect.targetHandle) {
|
||||
return;
|
||||
}
|
||||
if (connect.source === connect.target) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.Can not connect self')
|
||||
});
|
||||
}
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
},
|
||||
[onConnect, t, toast]
|
||||
);
|
||||
|
||||
/* edge */
|
||||
const onEdgeMouseEnter = useCallback(
|
||||
(e: any, edge: Edge) => {
|
||||
setHoverEdgeId(edge.id);
|
||||
},
|
||||
[setHoverEdgeId]
|
||||
);
|
||||
const onEdgeMouseLeave = useCallback(() => {
|
||||
setHoverEdgeId(undefined);
|
||||
}, [setHoverEdgeId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactFlow
|
||||
ref={reactFlowWrapper}
|
||||
fitView
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.1}
|
||||
maxZoom={1.5}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
elevateEdgesOnSelect
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgeChange}
|
||||
onConnect={customOnConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
>
|
||||
<FlowController />
|
||||
</ReactFlow>
|
||||
<ConfirmDeleteModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const Flow = ({ Header, ...data }: { Header: React.ReactNode }) => {
|
||||
const {
|
||||
isOpen: isOpenTemplate,
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
|
||||
const memoRenderContainer = useMemo(() => {
|
||||
return (
|
||||
<Box
|
||||
minH={'400px'}
|
||||
flex={'1 0 0'}
|
||||
w={'100%'}
|
||||
h={0}
|
||||
position={'relative'}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* open module template */}
|
||||
<IconButton
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
left={5}
|
||||
size={'mdSquare'}
|
||||
borderRadius={'50%'}
|
||||
icon={<SmallCloseIcon fontSize={'26px'} />}
|
||||
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
|
||||
transition={'0.2s ease'}
|
||||
aria-label={''}
|
||||
zIndex={1}
|
||||
boxShadow={'2px 2px 6px #85b1ff'}
|
||||
onClick={() => {
|
||||
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Container {...data} />
|
||||
|
||||
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
||||
</Box>
|
||||
);
|
||||
}, [data, isOpenTemplate, onCloseTemplate, onOpenTemplate]);
|
||||
|
||||
return (
|
||||
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
|
||||
<ReactFlowProvider>
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'myGray.50'}>
|
||||
{Header}
|
||||
{memoRenderContainer}
|
||||
</Flex>
|
||||
</ReactFlowProvider>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Flow);
|
||||
|
||||
const FlowController = React.memo(function FlowController() {
|
||||
const { fitView } = useReactFlow();
|
||||
return (
|
||||
<>
|
||||
<MiniMap
|
||||
style={{
|
||||
height: 78,
|
||||
width: 126,
|
||||
marginBottom: 35
|
||||
}}
|
||||
pannable
|
||||
/>
|
||||
<Controls
|
||||
position={'bottom-right'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
marginBottom: 5,
|
||||
background: 'white',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
boxShadow:
|
||||
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
|
||||
}}
|
||||
showInteractive={false}
|
||||
showFitView={false}
|
||||
>
|
||||
<MyTooltip label={'页面居中'}>
|
||||
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
|
||||
<MyIcon name={'core/modules/fixview'} w={'14px'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
</Controls>
|
||||
<Background />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -41,6 +41,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
|
||||
type EditProps = EditApiKeyProps & { _id?: string };
|
||||
const defaultEditData: EditProps = {
|
||||
@@ -84,8 +85,14 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
|
||||
<Box display={['block', 'flex']} py={[0, 3]} px={5} alignItems={'center'}>
|
||||
<MyBox
|
||||
isLoading={isGetting || isDeleting}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Box display={['block', 'flex']} alignItems={'center'}>
|
||||
<Box flex={1}>
|
||||
<Flex alignItems={'flex-end'}>
|
||||
<Box color={'myGray.900'} fontSize={'lg'}>
|
||||
@@ -103,7 +110,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} color={'myGray.600'}>
|
||||
<Box fontSize={'mini'} color={'myGray.600'}>
|
||||
{tips}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -140,7 +147,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<TableContainer mt={2} position={'relative'} minH={'300px'}>
|
||||
<TableContainer mt={3} position={'relative'} minH={'300px'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
@@ -225,7 +232,6 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Loading loading={isGetting || isDeleting} fixed={false} />
|
||||
</TableContainer>
|
||||
|
||||
{!!editData && (
|
||||
@@ -279,7 +285,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ const ConfigPerModal = ({
|
||||
<ModalBody>
|
||||
<HStack>
|
||||
<Avatar src={avatar} w={'1.75rem'} />
|
||||
<Box fontSize={'lg'}>{name}</Box>
|
||||
<Box>{name}</Box>
|
||||
</HStack>
|
||||
<Box mt={6}>
|
||||
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
|
||||
|
||||
@@ -3,7 +3,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import RowTabs from '../../../../common/Rowtabs';
|
||||
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
2
projects/app/src/global/core/app/api.d.ts
vendored
2
projects/app/src/global/core/app/api.d.ts
vendored
@@ -11,7 +11,6 @@ export type AppUpdateParams = {
|
||||
nodes?: AppSchema['modules'];
|
||||
edges?: AppSchema['edges'];
|
||||
chatConfig?: AppSchema['chatConfig'];
|
||||
permission?: AppSchema['permission'];
|
||||
teamTags?: AppSchema['teamTags'];
|
||||
defaultPermission?: AppSchema['defaultPermission'];
|
||||
};
|
||||
@@ -28,4 +27,5 @@ export type PostRevertAppProps = {
|
||||
// edit workflow
|
||||
editNodes: AppSchema['modules'];
|
||||
editEdges: AppSchema['edges'];
|
||||
editChatConfig: AppSchema['chatConfig'];
|
||||
};
|
||||
|
||||
@@ -6,8 +6,7 @@ export type PostWorkflowDebugProps = {
|
||||
nodes: RuntimeNodeItemType[];
|
||||
edges: RuntimeEdgeItemType[];
|
||||
variables: Record<string, any>;
|
||||
appId?: string;
|
||||
pluginId?: string;
|
||||
appId: string;
|
||||
};
|
||||
|
||||
export type PostWorkflowDebugResponse = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AppProps } from 'next/app';
|
||||
import Script from 'next/script';
|
||||
import Head from 'next/head';
|
||||
|
||||
import Layout from '@/components/Layout';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
|
||||
|
||||
@@ -2,10 +2,15 @@ import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import ApiKeyTable from '@/components/support/apikey/Table';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
const ApiKey = () => {
|
||||
const { publishT } = useI18n();
|
||||
return <ApiKeyTable tips={publishT('key tips')}></ApiKeyTable>;
|
||||
return (
|
||||
<Box px={[4, 8]} py={[4, 6]}>
|
||||
<ApiKeyTable tips={publishT('key tips')}></ApiKeyTable>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKey;
|
||||
|
||||
163
projects/app/src/pages/api/admin/initv485.ts
Normal file
163
projects/app/src/pages/api/admin/initv485.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
/*
|
||||
1. 先读取 HTTP plugin 内容,并找到所有的子plugin,然后事务批量创建,最后修改 inited
|
||||
2. 读取剩下未 inited 的plugin,逐一创建
|
||||
*/
|
||||
|
||||
let success = 0;
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await authCert({ req, authRoot: true });
|
||||
|
||||
const total = await MongoPlugin.countDocuments({
|
||||
inited: { $ne: true }
|
||||
});
|
||||
|
||||
console.log('Total plugin', total);
|
||||
|
||||
await initHttp();
|
||||
await initPlugin();
|
||||
}
|
||||
|
||||
async function initHttp(): Promise<any> {
|
||||
/* 读取http插件和他的children */
|
||||
const plugin = await MongoPlugin.findOne({
|
||||
type: PluginTypeEnum.folder,
|
||||
inited: { $ne: true }
|
||||
}).lean();
|
||||
if (!plugin) return;
|
||||
|
||||
const children = await MongoPlugin.find({
|
||||
teamId: plugin.teamId,
|
||||
parentId: plugin._id,
|
||||
inited: { $ne: true }
|
||||
}).lean();
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
/* 创建HTTP插件作为目录 */
|
||||
const [{ _id }] = await MongoApp.create(
|
||||
[
|
||||
{
|
||||
teamId: plugin.teamId,
|
||||
tmbId: plugin.tmbId,
|
||||
type: AppTypeEnum.httpPlugin,
|
||||
name: plugin.name,
|
||||
avatar: plugin.avatar,
|
||||
intro: plugin.intro,
|
||||
metadata: plugin.metadata,
|
||||
version: 'v2',
|
||||
pluginData: {
|
||||
apiSchemaStr: plugin.metadata?.apiSchemaStr,
|
||||
customHeaders: plugin.metadata?.customHeaders
|
||||
}
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
|
||||
/* 批量创建子插件 */
|
||||
for await (const item of children) {
|
||||
await MongoApp.create(
|
||||
[
|
||||
{
|
||||
parentId: _id,
|
||||
teamId: item.teamId,
|
||||
tmbId: item.tmbId,
|
||||
type: AppTypeEnum.plugin,
|
||||
name: item.name,
|
||||
avatar: item.avatar,
|
||||
intro: item.intro,
|
||||
version: 'v2',
|
||||
modules: item.modules,
|
||||
edges: item.edges,
|
||||
pluginData: {
|
||||
nodeVersion: item.nodeVersion,
|
||||
pluginUniId: plugin.metadata?.pluginUid
|
||||
}
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
|
||||
/* 更新插件信息 */
|
||||
await MongoPlugin.findOneAndUpdate(
|
||||
{
|
||||
_id: plugin._id
|
||||
},
|
||||
{
|
||||
$set: { inited: true }
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await MongoPlugin.updateMany(
|
||||
{
|
||||
teamId: plugin.teamId,
|
||||
parentId: plugin._id
|
||||
},
|
||||
{
|
||||
$set: { inited: true }
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
|
||||
success += children.length + 1;
|
||||
console.log(success);
|
||||
});
|
||||
|
||||
return initHttp();
|
||||
}
|
||||
|
||||
async function initPlugin(): Promise<any> {
|
||||
const plugin = await MongoPlugin.findOne({
|
||||
type: PluginTypeEnum.custom,
|
||||
inited: { $ne: true }
|
||||
}).lean();
|
||||
if (!plugin) return;
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
await MongoApp.create(
|
||||
[
|
||||
{
|
||||
teamId: plugin.teamId,
|
||||
tmbId: plugin.tmbId,
|
||||
type: AppTypeEnum.plugin,
|
||||
name: plugin.name,
|
||||
avatar: plugin.avatar,
|
||||
intro: plugin.intro,
|
||||
version: 'v2',
|
||||
modules: plugin.modules,
|
||||
edges: plugin.edges,
|
||||
pluginData: {
|
||||
nodeVersion: plugin.nodeVersion
|
||||
}
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
|
||||
await MongoPlugin.findOneAndUpdate(
|
||||
{
|
||||
_id: plugin._id
|
||||
},
|
||||
{
|
||||
$set: { inited: true }
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
|
||||
success++;
|
||||
console.log(success);
|
||||
});
|
||||
|
||||
return initPlugin();
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -12,6 +12,8 @@ import type { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
|
||||
import { defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { ClientSession } from '@fastgpt/service/common/mongo';
|
||||
|
||||
export type CreateAppBody = {
|
||||
parentId?: ParentIdType;
|
||||
@@ -35,37 +37,16 @@ async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse
|
||||
// 上限校验
|
||||
await checkTeamAppLimit(teamId);
|
||||
|
||||
// 创建模型
|
||||
const appId = await mongoSessionRun(async (session) => {
|
||||
const [{ _id: appId }] = await MongoApp.create(
|
||||
[
|
||||
{
|
||||
...parseParentIdInMongo(parentId),
|
||||
avatar,
|
||||
name,
|
||||
teamId,
|
||||
tmbId,
|
||||
modules,
|
||||
edges,
|
||||
type,
|
||||
version: 'v2'
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
|
||||
await MongoAppVersion.create(
|
||||
[
|
||||
{
|
||||
appId,
|
||||
nodes: modules,
|
||||
edges
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
|
||||
return appId;
|
||||
// 创建app
|
||||
const appId = await onCreateApp({
|
||||
parentId,
|
||||
name,
|
||||
avatar,
|
||||
type,
|
||||
modules,
|
||||
edges,
|
||||
teamId,
|
||||
tmbId
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
@@ -74,3 +55,72 @@ async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
export const onCreateApp = async ({
|
||||
parentId,
|
||||
name,
|
||||
intro,
|
||||
avatar,
|
||||
type,
|
||||
modules,
|
||||
edges,
|
||||
teamId,
|
||||
tmbId,
|
||||
pluginData,
|
||||
session
|
||||
}: {
|
||||
parentId?: ParentIdType;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
type?: AppTypeEnum;
|
||||
modules?: AppSchema['modules'];
|
||||
edges?: AppSchema['edges'];
|
||||
intro?: string;
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
pluginData?: AppSchema['pluginData'];
|
||||
session?: ClientSession;
|
||||
}) => {
|
||||
const create = async (session: ClientSession) => {
|
||||
const [{ _id: appId }] = await MongoApp.create(
|
||||
[
|
||||
{
|
||||
...parseParentIdInMongo(parentId),
|
||||
avatar,
|
||||
name,
|
||||
intro,
|
||||
teamId,
|
||||
tmbId,
|
||||
modules,
|
||||
edges,
|
||||
type,
|
||||
version: 'v2',
|
||||
pluginData,
|
||||
...(type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': defaultNodeVersion })
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
|
||||
if (type !== AppTypeEnum.folder && type !== AppTypeEnum.httpPlugin) {
|
||||
await MongoAppVersion.create(
|
||||
[
|
||||
{
|
||||
appId,
|
||||
nodes: modules,
|
||||
edges
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
|
||||
return appId;
|
||||
};
|
||||
|
||||
if (session) {
|
||||
return create(session);
|
||||
} else {
|
||||
return await mongoSessionRun(create);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@fastgpt/global/support/permission/constant';
|
||||
import { findAppAndAllChildren } from '@fastgpt/service/core/app/controller';
|
||||
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
|
||||
import { ClientSession } from '@fastgpt/service/common/mongo';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const { appId } = req.query as { appId: string };
|
||||
@@ -25,13 +26,30 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
// Auth owner (folder owner, can delete all apps in the folder)
|
||||
const { teamId } = await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
|
||||
|
||||
await onDelOneApp({
|
||||
teamId,
|
||||
appId
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
export const onDelOneApp = async ({
|
||||
teamId,
|
||||
appId,
|
||||
session
|
||||
}: {
|
||||
teamId: string;
|
||||
appId: string;
|
||||
session?: ClientSession;
|
||||
}) => {
|
||||
const apps = await findAppAndAllChildren({
|
||||
teamId,
|
||||
appId,
|
||||
fields: '_id'
|
||||
});
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
const del = async (session: ClientSession) => {
|
||||
for await (const app of apps) {
|
||||
const appId = app._id;
|
||||
// Chats
|
||||
@@ -83,7 +101,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default NextAPI(handler);
|
||||
if (session) {
|
||||
return del(session);
|
||||
}
|
||||
|
||||
return mongoSessionRun(del);
|
||||
};
|
||||
|
||||
68
projects/app/src/pages/api/core/app/httpPlugin/create.ts
Normal file
68
projects/app/src/pages/api/core/app/httpPlugin/create.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { httpApiSchema2Plugins } from '@fastgpt/global/core/app/httpPlugin/utils';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { onCreateApp, type CreateAppBody } from '../create';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
export type createHttpPluginQuery = {};
|
||||
|
||||
export type createHttpPluginBody = Omit<CreateAppBody, 'type' | 'modules' | 'edges'> & {
|
||||
intro?: string;
|
||||
pluginData: AppSchema['pluginData'];
|
||||
};
|
||||
|
||||
export type createHttpPluginResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<createHttpPluginBody, createHttpPluginQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<createHttpPluginResponse> {
|
||||
const { parentId, name, intro, avatar, pluginData } = req.body;
|
||||
|
||||
if (!name || !pluginData) {
|
||||
return Promise.reject('缺少参数');
|
||||
}
|
||||
|
||||
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
// create http plugin folder
|
||||
const httpPluginIid = await onCreateApp({
|
||||
parentId,
|
||||
name,
|
||||
avatar,
|
||||
intro,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: AppTypeEnum.httpPlugin,
|
||||
pluginData,
|
||||
session
|
||||
});
|
||||
|
||||
// compute children plugins
|
||||
const childrenPlugins = await httpApiSchema2Plugins({
|
||||
parentId: httpPluginIid,
|
||||
apiSchemaStr: pluginData.apiSchemaStr,
|
||||
customHeader: pluginData.customHeaders
|
||||
});
|
||||
|
||||
// create children plugins
|
||||
for await (const item of childrenPlugins) {
|
||||
await onCreateApp({
|
||||
...item,
|
||||
teamId,
|
||||
tmbId,
|
||||
session
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
127
projects/app/src/pages/api/core/app/httpPlugin/update.ts
Normal file
127
projects/app/src/pages/api/core/app/httpPlugin/update.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { NextApiResponse } from 'next';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { ClientSession } from '@fastgpt/service/common/mongo';
|
||||
import { httpApiSchema2Plugins } from '@fastgpt/global/core/app/httpPlugin/utils';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { isEqual } from 'lodash';
|
||||
import { onCreateApp } from '../create';
|
||||
import { onDelOneApp } from '../del';
|
||||
|
||||
export type UpdateHttpPluginBody = {
|
||||
appId: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
intro?: string;
|
||||
pluginData: AppSchema['pluginData'];
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<UpdateHttpPluginBody>, res: NextApiResponse<any>) {
|
||||
const { appId, name, avatar, intro, pluginData } = req.body;
|
||||
|
||||
const { app } = await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
|
||||
|
||||
const storeData = {
|
||||
apiSchemaStr: app.pluginData?.apiSchemaStr,
|
||||
customHeaders: app.pluginData?.customHeaders
|
||||
};
|
||||
const updateData = {
|
||||
apiSchemaStr: pluginData?.apiSchemaStr,
|
||||
customHeaders: pluginData?.customHeaders
|
||||
};
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
// update children
|
||||
if (!isEqual(storeData, updateData)) {
|
||||
await updateHttpChildrenPlugin({
|
||||
teamId: app.teamId,
|
||||
tmbId: app.tmbId,
|
||||
parentId: app._id,
|
||||
pluginData,
|
||||
session
|
||||
});
|
||||
}
|
||||
|
||||
await MongoApp.findByIdAndUpdate(
|
||||
appId,
|
||||
{
|
||||
name,
|
||||
avatar,
|
||||
intro,
|
||||
pluginData
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
const updateHttpChildrenPlugin = async ({
|
||||
teamId,
|
||||
tmbId,
|
||||
parentId,
|
||||
pluginData,
|
||||
session
|
||||
}: {
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
parentId: string;
|
||||
pluginData?: AppSchema['pluginData'];
|
||||
session: ClientSession;
|
||||
}) => {
|
||||
if (!pluginData?.apiSchemaStr) return;
|
||||
|
||||
const dbPlugins = await MongoApp.find({
|
||||
parentId,
|
||||
teamId
|
||||
}).select({
|
||||
pluginData: 1
|
||||
});
|
||||
|
||||
const schemaPlugins = await httpApiSchema2Plugins({
|
||||
parentId,
|
||||
apiSchemaStr: pluginData?.apiSchemaStr,
|
||||
customHeader: pluginData?.customHeaders
|
||||
});
|
||||
|
||||
// 数据库中存在,schema不存在,删除
|
||||
for await (const plugin of dbPlugins) {
|
||||
if (!schemaPlugins.find((p) => p.name === plugin.pluginData?.pluginUniId)) {
|
||||
await onDelOneApp({
|
||||
teamId,
|
||||
appId: plugin._id,
|
||||
session
|
||||
});
|
||||
}
|
||||
}
|
||||
// 数据库中不存在,schema存在,新增
|
||||
for await (const plugin of schemaPlugins) {
|
||||
if (!dbPlugins.find((p) => p.pluginData?.pluginUniId === plugin.name)) {
|
||||
await onCreateApp({
|
||||
...plugin,
|
||||
teamId,
|
||||
tmbId,
|
||||
session
|
||||
});
|
||||
}
|
||||
}
|
||||
// 数据库中存在,schema存在,更新
|
||||
for await (const plugin of schemaPlugins) {
|
||||
const dbPlugin = dbPlugins.find((p) => plugin.name === p.pluginData?.pluginUniId);
|
||||
if (dbPlugin) {
|
||||
await MongoApp.findByIdAndUpdate(
|
||||
dbPlugin._id,
|
||||
{
|
||||
...plugin,
|
||||
version: 'v2'
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -17,8 +17,9 @@ import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/
|
||||
|
||||
export type ListAppBody = {
|
||||
parentId?: ParentIdType;
|
||||
type?: AppTypeEnum;
|
||||
type?: AppTypeEnum | AppTypeEnum[];
|
||||
getRecentlyChat?: boolean;
|
||||
searchKey?: string;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
@@ -36,26 +37,43 @@ async function handler(
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
const { parentId, type, getRecentlyChat } = req.body;
|
||||
const { parentId, type, getRecentlyChat, searchKey } = req.body;
|
||||
|
||||
const findAppsQuery = getRecentlyChat
|
||||
? {
|
||||
const findAppsQuery = (() => {
|
||||
const searchMatch = searchKey
|
||||
? {
|
||||
$or: [
|
||||
{ name: { $regex: searchKey, $options: 'i' } },
|
||||
{ intro: { $regex: searchKey, $options: 'i' } }
|
||||
]
|
||||
}
|
||||
: {};
|
||||
|
||||
if (getRecentlyChat) {
|
||||
return {
|
||||
// get all chat app
|
||||
teamId,
|
||||
type: { $in: [AppTypeEnum.advanced, AppTypeEnum.simple] }
|
||||
}
|
||||
: {
|
||||
teamId,
|
||||
...(type && { type }),
|
||||
...parseParentIdInMongo(parentId)
|
||||
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple] },
|
||||
...searchMatch
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
teamId,
|
||||
...(type && Array.isArray(type) && { type: { $in: type } }),
|
||||
...(type && { type }),
|
||||
...parseParentIdInMongo(parentId),
|
||||
...searchMatch
|
||||
};
|
||||
})();
|
||||
|
||||
/* temp: get all apps and per */
|
||||
const [myApps, rpList] = await Promise.all([
|
||||
MongoApp.find(findAppsQuery, '_id avatar type name intro tmbId defaultPermission')
|
||||
MongoApp.find(findAppsQuery, '_id avatar type name intro tmbId pluginData defaultPermission')
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
.limit(searchKey ? 20 : 1000)
|
||||
.lean(),
|
||||
MongoResourcePermission.find({
|
||||
resourceType: PerResourceTypeEnum.app,
|
||||
@@ -88,7 +106,8 @@ async function handler(
|
||||
name: app.name,
|
||||
intro: app.intro,
|
||||
permission: app.permission,
|
||||
defaultPermission: app.defaultPermission || AppDefaultPermissionVal
|
||||
defaultPermission: app.defaultPermission || AppDefaultPermissionVal,
|
||||
pluginData: app.pluginData
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
33
projects/app/src/pages/api/core/app/plugin/getPreviewNode.ts
Normal file
33
projects/app/src/pages/api/core/app/plugin/getPreviewNode.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
get plugin preview modules
|
||||
*/
|
||||
import type { NextApiResponse } from 'next';
|
||||
import {
|
||||
getPluginPreviewNode,
|
||||
splitCombinePluginId
|
||||
} from '@fastgpt/service/core/app/plugin/controller';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
|
||||
export type GetPreviewNodeQuery = { appId: string };
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<{}, GetPreviewNodeQuery>,
|
||||
res: NextApiResponse<any>
|
||||
): Promise<FlowNodeTemplateType> {
|
||||
const { appId } = req.query;
|
||||
|
||||
const { source } = await splitCombinePluginId(appId);
|
||||
|
||||
if (source === PluginSourceEnum.personal) {
|
||||
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
|
||||
}
|
||||
|
||||
return getPluginPreviewNode({ id: appId });
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeTypeEnum, defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
intro: plugin.intro,
|
||||
showStatus: true,
|
||||
isTool: plugin.isTool,
|
||||
version: '481',
|
||||
version: defaultNodeVersion,
|
||||
inputs: [],
|
||||
outputs: []
|
||||
})) || [];
|
||||
53
projects/app/src/pages/api/core/app/transitionWorkflow.ts
Normal file
53
projects/app/src/pages/api/core/app/transitionWorkflow.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { onCreateApp } from './create';
|
||||
|
||||
export type transitionWorkflowQuery = {};
|
||||
|
||||
export type transitionWorkflowBody = {
|
||||
appId: string;
|
||||
createNew?: boolean;
|
||||
};
|
||||
|
||||
export type transitionWorkflowResponse = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<transitionWorkflowBody, transitionWorkflowQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<transitionWorkflowResponse> {
|
||||
const { appId, createNew } = req.body;
|
||||
|
||||
const { app, tmbId } = await authApp({
|
||||
req,
|
||||
appId,
|
||||
authToken: true,
|
||||
per: OwnerPermissionVal
|
||||
});
|
||||
|
||||
if (createNew) {
|
||||
const appId = await onCreateApp({
|
||||
parentId: app.parentId,
|
||||
name: app.name + ' Copy',
|
||||
avatar: app.avatar,
|
||||
type: AppTypeEnum.workflow,
|
||||
modules: app.modules,
|
||||
edges: app.edges,
|
||||
teamId: app.teamId,
|
||||
tmbId
|
||||
});
|
||||
|
||||
return { id: appId };
|
||||
} else {
|
||||
await MongoApp.findByIdAndUpdate(appId, { type: AppTypeEnum.workflow });
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -6,8 +6,7 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import {
|
||||
ManagePermissionVal,
|
||||
WritePermissionVal,
|
||||
OwnerPermissionVal
|
||||
WritePermissionVal
|
||||
} from '@fastgpt/global/support/permission/constant';
|
||||
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
|
||||
|
||||
@@ -22,7 +21,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig,
|
||||
permission,
|
||||
teamTags,
|
||||
defaultPermission
|
||||
} = req.body as AppUpdateParams;
|
||||
@@ -33,9 +31,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
if (permission) {
|
||||
await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
|
||||
} else if (defaultPermission) {
|
||||
if (defaultPermission) {
|
||||
await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
|
||||
} else {
|
||||
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
|
||||
@@ -56,7 +52,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
type,
|
||||
avatar,
|
||||
intro,
|
||||
permission,
|
||||
defaultPermission,
|
||||
...(teamTags && teamTags),
|
||||
...(formatNodes && {
|
||||
|
||||
36
projects/app/src/pages/api/core/app/version/latest.ts
Normal file
36
projects/app/src/pages/api/core/app/version/latest.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
|
||||
export type getLatestVersionQuery = {
|
||||
appId: string;
|
||||
};
|
||||
|
||||
export type getLatestVersionBody = {};
|
||||
|
||||
export type getLatestVersionResponse = {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
chatConfig: AppChatConfigType;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<getLatestVersionBody, getLatestVersionQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<getLatestVersionResponse> {
|
||||
const { app } = await authApp({
|
||||
req,
|
||||
authToken: true,
|
||||
appId: req.query.appId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
return getAppLatestVersion(req.query.appId, app);
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -8,6 +8,7 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
|
||||
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
|
||||
import { PostPublishAppProps } from '@/global/core/app/api';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
type Response = {};
|
||||
|
||||
@@ -15,13 +16,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
|
||||
const { appId } = req.query as { appId: string };
|
||||
const { nodes = [], edges = [], chatConfig, type } = req.body as PostPublishAppProps;
|
||||
|
||||
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
const { app } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
|
||||
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
// create version histories
|
||||
await MongoAppVersion.create(
|
||||
const [{ _id }] = await MongoAppVersion.create(
|
||||
[
|
||||
{
|
||||
appId,
|
||||
@@ -34,18 +35,25 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
|
||||
);
|
||||
|
||||
// update app
|
||||
await MongoApp.findByIdAndUpdate(appId, {
|
||||
modules: formatNodes,
|
||||
edges,
|
||||
chatConfig,
|
||||
updateTime: new Date(),
|
||||
version: 'v2',
|
||||
type,
|
||||
scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
|
||||
scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig
|
||||
? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
|
||||
: null
|
||||
});
|
||||
await MongoApp.findByIdAndUpdate(
|
||||
appId,
|
||||
{
|
||||
modules: formatNodes,
|
||||
edges,
|
||||
chatConfig,
|
||||
updateTime: new Date(),
|
||||
version: 'v2',
|
||||
type,
|
||||
scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
|
||||
scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig?.cronString
|
||||
? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
|
||||
: null,
|
||||
...(app.type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': _id })
|
||||
},
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {};
|
||||
|
||||
@@ -8,14 +8,20 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
|
||||
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
|
||||
import { PostRevertAppProps } from '@/global/core/app/api';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
type Response = {};
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> {
|
||||
const { appId } = req.query as { appId: string };
|
||||
const { editNodes = [], editEdges = [], versionId } = req.body as PostRevertAppProps;
|
||||
const {
|
||||
editNodes = [],
|
||||
editEdges = [],
|
||||
editChatConfig,
|
||||
versionId
|
||||
} = req.body as PostRevertAppProps;
|
||||
|
||||
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
const { app } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
|
||||
const version = await MongoAppVersion.findOne({
|
||||
_id: versionId,
|
||||
@@ -37,19 +43,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
|
||||
{
|
||||
appId,
|
||||
nodes: formatEditNodes,
|
||||
edges: editEdges
|
||||
edges: editEdges,
|
||||
chatConfig: editChatConfig
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
|
||||
// 为历史版本再创建一个版本
|
||||
await MongoAppVersion.create(
|
||||
const [{ _id }] = await MongoAppVersion.create(
|
||||
[
|
||||
{
|
||||
appId,
|
||||
nodes: version.nodes,
|
||||
edges: version.edges
|
||||
edges: version.edges,
|
||||
chatConfig: version.chatConfig
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
@@ -59,11 +67,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
|
||||
await MongoApp.findByIdAndUpdate(appId, {
|
||||
modules: version.nodes,
|
||||
edges: version.edges,
|
||||
chatConfig: version.chatConfig,
|
||||
updateTime: new Date(),
|
||||
scheduledTriggerConfig,
|
||||
scheduledTriggerNextTime: scheduledTriggerConfig
|
||||
scheduledTriggerNextTime: scheduledTriggerConfig?.cronString
|
||||
? getNextTimeByCronStringAndTimezone(scheduledTriggerConfig)
|
||||
: null
|
||||
: null,
|
||||
...(app.type === AppTypeEnum.plugin && { 'pluginData.nodeVersion': _id })
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { authChatCert } from '@/service/support/permission/auth/chat';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
|
||||
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
export type QueryChatInputGuideBody = OutLinkChatAuthProps & {
|
||||
appId: string;
|
||||
@@ -28,7 +29,7 @@ async function handler(
|
||||
|
||||
const params = {
|
||||
appId,
|
||||
...(searchKey && { text: { $regex: new RegExp(searchKey, 'i') } })
|
||||
...(searchKey && { text: { $regex: new RegExp(`${replaceRegChars(searchKey)}`, 'i') } })
|
||||
};
|
||||
|
||||
const result = await MongoChatInputGuide.find(params).sort({ _id: -1 }).limit(6);
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import type { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { httpApiSchema2Plugins } from '@fastgpt/global/core/plugin/httpPlugin/utils';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
|
||||
const body = req.body as CreateOnePluginParams;
|
||||
|
||||
// await checkTeamPluginLimit(teamId);
|
||||
|
||||
// create parent plugin and child plugin
|
||||
if (body.metadata?.apiSchemaStr) {
|
||||
const parentId = await mongoSessionRun(async (session) => {
|
||||
const [{ _id: parentId }] = await MongoPlugin.create(
|
||||
[
|
||||
{
|
||||
...body,
|
||||
parentId: null,
|
||||
teamId,
|
||||
tmbId,
|
||||
version: 'v2'
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
);
|
||||
|
||||
const childrenPlugins = await httpApiSchema2Plugins({
|
||||
parentId,
|
||||
apiSchemaStr: body.metadata?.apiSchemaStr,
|
||||
customHeader: body.metadata?.customHeaders
|
||||
});
|
||||
|
||||
await MongoPlugin.create(
|
||||
childrenPlugins.map((item) => ({
|
||||
...item,
|
||||
metadata: {
|
||||
pluginUid: item.name
|
||||
},
|
||||
teamId,
|
||||
tmbId,
|
||||
version: 'v2'
|
||||
})),
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
return parentId;
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: parentId
|
||||
});
|
||||
} else {
|
||||
const { _id } = await MongoPlugin.create({
|
||||
...body,
|
||||
teamId,
|
||||
tmbId,
|
||||
version: 'v2'
|
||||
});
|
||||
jsonRes(res, {
|
||||
data: _id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
|
||||
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { teamId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
|
||||
const { pluginId } = req.query as { pluginId: string };
|
||||
|
||||
if (!pluginId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
await authPluginCrud({ req, authToken: true, pluginId, per: 'owner' });
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
await MongoPlugin.deleteMany(
|
||||
{
|
||||
teamId,
|
||||
parentId: pluginId
|
||||
},
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
await MongoPlugin.findByIdAndDelete(pluginId, { session });
|
||||
});
|
||||
|
||||
jsonRes(res, {});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { id } = req.query as { id: string };
|
||||
await connectToDatabase();
|
||||
const { plugin } = await authPluginCrud({ req, authToken: true, pluginId: id, per: 'r' });
|
||||
|
||||
jsonRes(res, {
|
||||
data: plugin
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
get plugin preview modules
|
||||
*/
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getPluginPreviewNode } from '@fastgpt/service/core/plugin/controller';
|
||||
import { authPluginCanUse } from '@fastgpt/service/support/permission/auth/plugin';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { id } = req.query as { id: string };
|
||||
await connectToDatabase();
|
||||
const { teamId, tmbId } = await authCert({ req, authToken: true });
|
||||
await authPluginCanUse({ id, teamId, tmbId });
|
||||
|
||||
jsonRes<FlowNodeTemplateType>(res, {
|
||||
data: await getPluginPreviewNode({ id })
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
|
||||
import { PluginListItemType } from '@fastgpt/global/core/plugin/controller';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { parentId, type } = req.query as { parentId?: string; type?: DatasetTypeEnum };
|
||||
|
||||
const { teamId } = await authCert({ req, authToken: true });
|
||||
|
||||
const plugins = await MongoPlugin.find(
|
||||
{
|
||||
teamId,
|
||||
...(parentId !== undefined && { parentId: parentId || null }),
|
||||
...(type && { type })
|
||||
},
|
||||
'_id parentId type name avatar intro metadata'
|
||||
)
|
||||
.sort({ updateTime: -1 })
|
||||
.lean();
|
||||
|
||||
jsonRes<PluginListItemType[]>(res, {
|
||||
data: plugins
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import type { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type.d';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { parentId } = req.query as { parentId: string };
|
||||
|
||||
if (!parentId) {
|
||||
return jsonRes(res, {
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
await authCert({ req, authToken: true });
|
||||
|
||||
jsonRes<ParentTreePathItemType[]>(res, {
|
||||
data: await getParents(parentId)
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getParents(parentId?: string): Promise<ParentTreePathItemType[]> {
|
||||
if (!parentId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parent = await MongoPlugin.findById(parentId, 'name parentId');
|
||||
|
||||
if (!parent) return [];
|
||||
|
||||
const paths = await getParents(parent.parentId);
|
||||
paths.push({ parentId, parentName: parent.name });
|
||||
|
||||
return paths;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { parentId, searchKey } = req.query as { parentId?: string; searchKey?: string };
|
||||
const { teamId } = await authCert({ req, authToken: true });
|
||||
|
||||
const userPlugins = await (async () => {
|
||||
if (searchKey) {
|
||||
return MongoPlugin.find({
|
||||
teamId,
|
||||
// search name or intro
|
||||
$or: [
|
||||
{ name: { $regex: searchKey, $options: 'i' } },
|
||||
{ intro: { $regex: searchKey, $options: 'i' } }
|
||||
]
|
||||
})
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
.lean();
|
||||
} else {
|
||||
return MongoPlugin.find({ teamId, parentId: parentId || null })
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
.lean();
|
||||
}
|
||||
})();
|
||||
|
||||
const data: FlowNodeTemplateType[] = userPlugins.map((plugin) => ({
|
||||
id: String(plugin._id),
|
||||
parentId: String(plugin.parentId),
|
||||
pluginId: String(plugin._id),
|
||||
pluginType: plugin.type,
|
||||
templateType: FlowNodeTemplateTypeEnum.personalPlugin,
|
||||
flowNodeType: FlowNodeTypeEnum.pluginModule,
|
||||
avatar: plugin.avatar,
|
||||
name: plugin.name,
|
||||
intro: plugin.intro,
|
||||
showStatus: false,
|
||||
version: '481',
|
||||
inputs: [],
|
||||
outputs: []
|
||||
}));
|
||||
|
||||
jsonRes<FlowNodeTemplateType[]>(res, {
|
||||
data
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import type { UpdatePluginParams } from '@fastgpt/global/core/plugin/controller';
|
||||
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
|
||||
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { ClientSession } from '@fastgpt/service/common/mongo';
|
||||
import { httpApiSchema2Plugins } from '@fastgpt/global/core/plugin/httpPlugin/utils';
|
||||
import { isEqual } from 'lodash';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const body = req.body as UpdatePluginParams;
|
||||
|
||||
const { id, modules, edges, ...props } = body;
|
||||
|
||||
const { teamId, tmbId } = await authPluginCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
pluginId: id,
|
||||
per: 'owner'
|
||||
});
|
||||
|
||||
const originPlugin = await MongoPlugin.findById(id);
|
||||
|
||||
let updateData = {
|
||||
name: props.name,
|
||||
intro: props.intro,
|
||||
avatar: props.avatar,
|
||||
parentId: props.parentId,
|
||||
version: 'v2',
|
||||
...(modules?.length && {
|
||||
modules: modules
|
||||
}),
|
||||
...(edges?.length && { edges }),
|
||||
metadata: props.metadata,
|
||||
nodeVersion: originPlugin?.nodeVersion
|
||||
};
|
||||
|
||||
const isNodeVersionEqual =
|
||||
isEqual(
|
||||
originPlugin?.modules.map((module) => {
|
||||
return { ...module, position: undefined };
|
||||
}),
|
||||
updateData.modules?.map((module) => {
|
||||
return { ...module, position: undefined };
|
||||
})
|
||||
) && isEqual(originPlugin?.edges, updateData.edges);
|
||||
|
||||
if (!isNodeVersionEqual) {
|
||||
updateData = {
|
||||
...updateData,
|
||||
nodeVersion: nanoid(6)
|
||||
};
|
||||
}
|
||||
if (props.metadata?.apiSchemaStr) {
|
||||
await mongoSessionRun(async (session) => {
|
||||
// update children
|
||||
await updateHttpChildrenPlugin({
|
||||
teamId,
|
||||
tmbId,
|
||||
parent: body,
|
||||
session
|
||||
});
|
||||
await MongoPlugin.findByIdAndUpdate(id, updateData, { session });
|
||||
});
|
||||
|
||||
jsonRes(res, {});
|
||||
} else {
|
||||
jsonRes(res, {
|
||||
data: await MongoPlugin.findByIdAndUpdate(id, updateData)
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updateHttpChildrenPlugin = async ({
|
||||
teamId,
|
||||
tmbId,
|
||||
parent,
|
||||
session
|
||||
}: {
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
parent: UpdatePluginParams;
|
||||
session: ClientSession;
|
||||
}) => {
|
||||
if (!parent.metadata?.apiSchemaStr) return;
|
||||
const dbPlugins = await MongoPlugin.find(
|
||||
{
|
||||
parentId: parent.id,
|
||||
teamId
|
||||
},
|
||||
'_id metadata'
|
||||
);
|
||||
|
||||
const schemaPlugins = await httpApiSchema2Plugins({
|
||||
parentId: parent.id,
|
||||
apiSchemaStr: parent.metadata?.apiSchemaStr,
|
||||
customHeader: parent.metadata?.customHeaders
|
||||
});
|
||||
|
||||
// 数据库中存在,schema不存在,删除
|
||||
for await (const plugin of dbPlugins) {
|
||||
if (!schemaPlugins.find((p) => p.name === plugin.metadata?.pluginUid)) {
|
||||
await MongoPlugin.deleteOne({ _id: plugin._id }, { session });
|
||||
}
|
||||
}
|
||||
// 数据库中不存在,schema存在,新增
|
||||
for await (const plugin of schemaPlugins) {
|
||||
if (!dbPlugins.find((p) => p.metadata?.pluginUid === plugin.name)) {
|
||||
await MongoPlugin.create(
|
||||
[
|
||||
{
|
||||
...plugin,
|
||||
metadata: {
|
||||
pluginUid: plugin.name
|
||||
},
|
||||
teamId,
|
||||
tmbId,
|
||||
version: 'v2'
|
||||
}
|
||||
],
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// 数据库中存在,schema存在,更新
|
||||
for await (const plugin of schemaPlugins) {
|
||||
const dbPlugin = dbPlugins.find((p) => plugin.name === p.metadata?.pluginUid);
|
||||
if (dbPlugin) {
|
||||
await MongoPlugin.findByIdAndUpdate(
|
||||
dbPlugin._id,
|
||||
{
|
||||
...plugin,
|
||||
version: 'v2'
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
|
||||
import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
|
||||
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { defaultApp } from '@/web/core/app/constants';
|
||||
@@ -15,13 +14,7 @@ async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<PostWorkflowDebugResponse> {
|
||||
const {
|
||||
nodes = [],
|
||||
edges = [],
|
||||
variables = {},
|
||||
appId,
|
||||
pluginId
|
||||
} = req.body as PostWorkflowDebugProps;
|
||||
const { nodes = [], edges = [], variables = {}, appId } = req.body as PostWorkflowDebugProps;
|
||||
|
||||
if (!nodes) {
|
||||
throw new Error('Prams Error');
|
||||
@@ -39,8 +32,7 @@ async function handler(
|
||||
req,
|
||||
authToken: true
|
||||
}),
|
||||
appId && authApp({ req, authToken: true, appId, per: ReadPermissionVal }),
|
||||
pluginId && authPluginCrud({ req, authToken: true, pluginId, per: 'r' })
|
||||
authApp({ req, authToken: true, appId, per: ReadPermissionVal })
|
||||
]);
|
||||
|
||||
// auth balance
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Box, Flex, IconButton, useTheme, useDisclosure, Button } from '@chakra-ui/react';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import ChatTest, { type ChatTestComponentRef } from '@/components/core/workflow/Flow/ChatTest';
|
||||
import { uiWorkflow2StoreWorkflow } from '@/components/core/workflow/utils';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import {
|
||||
checkWorkflowNodeAndConnection,
|
||||
filterSensitiveNodesData
|
||||
} from '@/web/core/workflow/utils';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { formatTime2HM } from '@fastgpt/global/common/string/time';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '@/components/core/workflow/context';
|
||||
import { useInterval, useUpdateEffect } from 'ahooks';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
const ImportSettings = dynamic(() => import('@/components/core/workflow/Flow/ImportSettings'));
|
||||
const PublishHistories = dynamic(
|
||||
() => import('@/components/core/workflow/components/PublishHistoriesSlider')
|
||||
);
|
||||
|
||||
type Props = { onClose: () => void };
|
||||
|
||||
const RenderHeaderContainer = React.memo(function RenderHeaderContainer({
|
||||
ChatTestRef,
|
||||
setWorkflowTestData,
|
||||
onClose
|
||||
}: Props & {
|
||||
ChatTestRef: React.RefObject<ChatTestComponentRef>;
|
||||
setWorkflowTestData: React.Dispatch<
|
||||
React.SetStateAction<
|
||||
| {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
}
|
||||
| undefined
|
||||
>
|
||||
>;
|
||||
}) {
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { copyData } = useCopyData();
|
||||
const { openConfirm: openConfigPublish, ConfirmModal } = useConfirm({
|
||||
content: t('core.app.Publish Confirm')
|
||||
});
|
||||
const { publishApp, updateAppDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveLabel, setSaveLabel] = useState(t('core.app.Onclick to save'));
|
||||
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
||||
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
|
||||
const isShowVersionHistories = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v.isShowVersionHistories
|
||||
);
|
||||
const setIsShowVersionHistories = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v.setIsShowVersionHistories
|
||||
);
|
||||
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
|
||||
|
||||
const flowData2StoreDataAndCheck = useCallback(async () => {
|
||||
const { nodes } = await getWorkflowStore();
|
||||
const checkResults = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||
|
||||
if (!checkResults) {
|
||||
const storeNodes = uiWorkflow2StoreWorkflow({ nodes, edges });
|
||||
|
||||
return storeNodes;
|
||||
} else {
|
||||
checkResults.forEach((nodeId) => onUpdateNodeError(nodeId, true));
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('core.workflow.Check Failed')
|
||||
});
|
||||
}
|
||||
}, [edges, onUpdateNodeError, t, toast]);
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (forbid?: boolean) => {
|
||||
// version preview / debug mode, not save
|
||||
if (!isV2Workflow || isShowVersionHistories || forbid) return;
|
||||
|
||||
const { nodes } = await getWorkflowStore();
|
||||
|
||||
if (nodes.length === 0) return null;
|
||||
setIsSaving(true);
|
||||
|
||||
const storeWorkflow = uiWorkflow2StoreWorkflow({ nodes, edges });
|
||||
|
||||
try {
|
||||
await updateAppDetail({
|
||||
...storeWorkflow,
|
||||
type: AppTypeEnum.advanced,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
|
||||
setSaveLabel(
|
||||
t('core.app.Auto Save time', {
|
||||
time: formatTime2HM()
|
||||
})
|
||||
);
|
||||
// ChatTestRef.current?.resetChatTest();
|
||||
} catch (error) {}
|
||||
|
||||
setIsSaving(false);
|
||||
|
||||
return null;
|
||||
},
|
||||
[isV2Workflow, isShowVersionHistories, edges, updateAppDetail, appDetail.chatConfig, t]
|
||||
);
|
||||
|
||||
const onclickPublish = useCallback(async () => {
|
||||
setIsSaving(true);
|
||||
const data = await flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
try {
|
||||
await publishApp({
|
||||
...data,
|
||||
type: AppTypeEnum.advanced,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('core.app.Publish Success')
|
||||
});
|
||||
ChatTestRef.current?.resetChatTest();
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, t('core.app.Publish Failed'))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setIsSaving(false);
|
||||
}, [flowData2StoreDataAndCheck, publishApp, appDetail.chatConfig, toast, t, ChatTestRef]);
|
||||
|
||||
const saveAndBack = useCallback(async () => {
|
||||
try {
|
||||
await onclickSave();
|
||||
onClose();
|
||||
} catch (error) {}
|
||||
}, [onClose, onclickSave]);
|
||||
|
||||
const onExportWorkflow = useCallback(async () => {
|
||||
const data = await flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
copyData(
|
||||
JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
appT('Export Config Successful')
|
||||
);
|
||||
}
|
||||
}, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
|
||||
|
||||
// effect
|
||||
useBeforeunload({
|
||||
callback: onclickSave,
|
||||
tip: t('core.common.tip.leave page')
|
||||
});
|
||||
|
||||
useInterval(() => {
|
||||
if (!appDetail._id) return;
|
||||
onclickSave(!!workflowDebugData);
|
||||
}, 20000);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
py={3}
|
||||
px={[2, 5, 8]}
|
||||
borderBottom={theme.borders.base}
|
||||
alignItems={'center'}
|
||||
userSelect={'none'}
|
||||
bg={'myGray.25'}
|
||||
h={'67px'}
|
||||
>
|
||||
<IconButton
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/backFill'} w={'14px'} />}
|
||||
borderRadius={'50%'}
|
||||
w={'26px'}
|
||||
h={'26px'}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'whiteBase'}
|
||||
aria-label={''}
|
||||
isLoading={isSaving}
|
||||
onClick={saveAndBack}
|
||||
/>
|
||||
<Box ml={[2, 4]}>
|
||||
<Box fontSize={'md'} fontWeight={'bold'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
{!isShowVersionHistories && isV2Workflow && (
|
||||
<MyTooltip label={t('core.app.Onclick to save')}>
|
||||
<Box
|
||||
fontSize={'xs'}
|
||||
mt={1}
|
||||
display={'inline-block'}
|
||||
borderRadius={'xs'}
|
||||
cursor={'pointer'}
|
||||
onClick={() => onclickSave()}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
{saveLabel}
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flex={1} />
|
||||
|
||||
{!isShowVersionHistories && (
|
||||
<>
|
||||
<MyMenu
|
||||
Button={
|
||||
<IconButton
|
||||
mr={[2, 4]}
|
||||
icon={<MyIcon name={'more'} w={'14px'} p={2} />}
|
||||
aria-label={''}
|
||||
size={'sm'}
|
||||
variant={'whitePrimary'}
|
||||
/>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: appT('Import Configs'),
|
||||
icon: 'common/importLight',
|
||||
onClick: onOpenImport
|
||||
},
|
||||
{
|
||||
label: appT('Export Configs'),
|
||||
icon: 'export',
|
||||
onClick: onExportWorkflow
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
mr={[2, 4]}
|
||||
icon={<MyIcon name={'history'} w={'18px'} />}
|
||||
aria-label={''}
|
||||
size={'sm'}
|
||||
w={'30px'}
|
||||
variant={'whitePrimary'}
|
||||
onClick={() => setIsShowVersionHistories(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
|
||||
variant={'whitePrimary'}
|
||||
onClick={async () => {
|
||||
const data = await flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
setWorkflowTestData(data);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('core.workflow.Debug')}
|
||||
</Button>
|
||||
|
||||
{!isShowVersionHistories && (
|
||||
<Button
|
||||
ml={[2, 4]}
|
||||
size={'sm'}
|
||||
isLoading={isSaving}
|
||||
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
|
||||
onClick={openConfigPublish(onclickPublish)}
|
||||
>
|
||||
{t('core.app.Publish')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<ConfirmModal confirmText={t('core.app.Publish')} />
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
theme.borders.base,
|
||||
isSaving,
|
||||
saveAndBack,
|
||||
appDetail.name,
|
||||
isShowVersionHistories,
|
||||
isV2Workflow,
|
||||
t,
|
||||
saveLabel,
|
||||
appT,
|
||||
onOpenImport,
|
||||
onExportWorkflow,
|
||||
openConfigPublish,
|
||||
onclickPublish,
|
||||
ConfirmModal,
|
||||
onclickSave,
|
||||
setIsShowVersionHistories,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Render}
|
||||
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
|
||||
{isShowVersionHistories && <PublishHistories />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const Header = (props: Props) => {
|
||||
const ChatTestRef = useRef<ChatTestComponentRef>(null);
|
||||
|
||||
const [workflowTestData, setWorkflowTestData] = useState<{
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
}>();
|
||||
const { isOpen: isOpenTest, onOpen: onOpenTest, onClose: onCloseTest } = useDisclosure();
|
||||
|
||||
useUpdateEffect(() => {
|
||||
onOpenTest();
|
||||
}, [workflowTestData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RenderHeaderContainer
|
||||
{...props}
|
||||
ChatTestRef={ChatTestRef}
|
||||
setWorkflowTestData={setWorkflowTestData}
|
||||
/>
|
||||
<ChatTest ref={ChatTestRef} isOpen={isOpenTest} {...workflowTestData} onClose={onCloseTest} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Header);
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
getCollaboratorList
|
||||
} from '@/web/core/app/api/collaborator';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import {
|
||||
AppDefaultPermissionVal,
|
||||
AppPermissionList
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
Tbody,
|
||||
useTheme,
|
||||
useDisclosure,
|
||||
ModalBody
|
||||
ModalBody,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -35,13 +36,17 @@ import { formatChatValue2InputType } from '@/components/ChatBox/utils';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { cardStyles } from '../constants';
|
||||
|
||||
const Logs = ({ appId }: { appId: string }) => {
|
||||
const Logs = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { isPc } = useSystemStore();
|
||||
|
||||
const appId = useContextSelector(AppContext, (v) => v.appId);
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRangeType>({
|
||||
from: addDays(new Date(), -7),
|
||||
to: new Date()
|
||||
@@ -72,8 +77,8 @@ const Logs = ({ appId }: { appId: string }) => {
|
||||
const [detailLogsId, setDetailLogsId] = useState<string>();
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} pt={[1, 5]} position={'relative'}>
|
||||
<Box px={[4, 8]}>
|
||||
<>
|
||||
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
|
||||
{isPc && (
|
||||
<>
|
||||
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mb={2}>
|
||||
@@ -96,97 +101,106 @@ const Logs = ({ appId }: { appId: string }) => {
|
||||
</Box>
|
||||
|
||||
{/* table */}
|
||||
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'} px={[4, 8]}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('core.app.logs.Source And Time')}</Th>
|
||||
<Th>{appT('Logs Title')}</Th>
|
||||
<Th>{appT('Logs Message Total')}</Th>
|
||||
<Th>{appT('Feedback Count')}</Th>
|
||||
<Th>{t('core.app.feedback.Custom feedback')}</Th>
|
||||
<Th>{appT('Mark Count')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'xs'}>
|
||||
{logs.map((item) => (
|
||||
<Tr
|
||||
key={item._id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={'点击查看对话详情'}
|
||||
onClick={() => setDetailLogsId(item.id)}
|
||||
>
|
||||
<Td>
|
||||
<Box>{t(ChatSourceMap[item.source]?.name || 'UnKnow')}</Box>
|
||||
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
|
||||
</Td>
|
||||
<Td className="textEllipsis" maxW={'250px'}>
|
||||
{item.title}
|
||||
</Td>
|
||||
<Td>{item.messageCount}</Td>
|
||||
<Td w={'100px'}>
|
||||
{!!item?.userGoodFeedbackCount && (
|
||||
<Flex
|
||||
mb={item?.userGoodFeedbackCount ? 1 : 0}
|
||||
bg={'green.100'}
|
||||
color={'green.600'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
borderRadius={'md'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'core/chat/feedback/goodLight'}
|
||||
color={'green.600'}
|
||||
w={'14px'}
|
||||
/>
|
||||
{item.userGoodFeedbackCount}
|
||||
</Flex>
|
||||
)}
|
||||
{!!item?.userBadFeedbackCount && (
|
||||
<Flex
|
||||
bg={'#FFF2EC'}
|
||||
color={'#C96330'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
borderRadius={'md'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'core/chat/feedback/badLight'}
|
||||
color={'#C96330'}
|
||||
w={'14px'}
|
||||
/>
|
||||
{item.userBadFeedbackCount}
|
||||
</Flex>
|
||||
)}
|
||||
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
|
||||
</Td>
|
||||
<Td>{item.customFeedbacksCount || '-'}</Td>
|
||||
<Td>{item.markCount}</Td>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
{...cardStyles}
|
||||
boxShadow={3.5}
|
||||
mt={4}
|
||||
px={[4, 8]}
|
||||
py={[4, 6]}
|
||||
flex={'1 0 0'}
|
||||
>
|
||||
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
<Table variant={'simple'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('core.app.logs.Source And Time')}</Th>
|
||||
<Th>{appT('Logs Title')}</Th>
|
||||
<Th>{appT('Logs Message Total')}</Th>
|
||||
<Th>{appT('Feedback Count')}</Th>
|
||||
<Th>{t('core.app.feedback.Custom feedback')}</Th>
|
||||
<Th>{appT('Mark Count')}</Th>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{logs.length === 0 && !isLoading && <EmptyTip text={appT('Logs Empty')}></EmptyTip>}
|
||||
<Flex w={'100%'} p={4} alignItems={'center'} justifyContent={'flex-end'}>
|
||||
<DateRangePicker
|
||||
defaultDate={dateRange}
|
||||
position="top"
|
||||
onChange={setDateRange}
|
||||
onSuccess={() => getData(1)}
|
||||
/>
|
||||
<Box ml={3}>
|
||||
</Thead>
|
||||
<Tbody fontSize={'xs'}>
|
||||
{logs.map((item) => (
|
||||
<Tr
|
||||
key={item._id}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
cursor={'pointer'}
|
||||
title={'点击查看对话详情'}
|
||||
onClick={() => setDetailLogsId(item.id)}
|
||||
>
|
||||
<Td>
|
||||
<Box>{t(ChatSourceMap[item.source]?.name || 'UnKnow')}</Box>
|
||||
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
|
||||
</Td>
|
||||
<Td className="textEllipsis" maxW={'250px'}>
|
||||
{item.title}
|
||||
</Td>
|
||||
<Td>{item.messageCount}</Td>
|
||||
<Td w={'100px'}>
|
||||
{!!item?.userGoodFeedbackCount && (
|
||||
<Flex
|
||||
mb={item?.userGoodFeedbackCount ? 1 : 0}
|
||||
bg={'green.100'}
|
||||
color={'green.600'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
borderRadius={'md'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'core/chat/feedback/goodLight'}
|
||||
color={'green.600'}
|
||||
w={'14px'}
|
||||
/>
|
||||
{item.userGoodFeedbackCount}
|
||||
</Flex>
|
||||
)}
|
||||
{!!item?.userBadFeedbackCount && (
|
||||
<Flex
|
||||
bg={'#FFF2EC'}
|
||||
color={'#C96330'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
borderRadius={'md'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon
|
||||
mr={1}
|
||||
name={'core/chat/feedback/badLight'}
|
||||
color={'#C96330'}
|
||||
w={'14px'}
|
||||
/>
|
||||
{item.userBadFeedbackCount}
|
||||
</Flex>
|
||||
)}
|
||||
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
|
||||
</Td>
|
||||
<Td>{item.customFeedbacksCount || '-'}</Td>
|
||||
<Td>{item.markCount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{logs.length === 0 && !isLoading && <EmptyTip text={appT('Logs Empty')}></EmptyTip>}
|
||||
</TableContainer>
|
||||
|
||||
<HStack w={'100%'} mt={3} justifyContent={'flex-end'}>
|
||||
<DateRangePicker
|
||||
defaultDate={dateRange}
|
||||
position="top"
|
||||
onChange={setDateRange}
|
||||
onSuccess={() => getData(1)}
|
||||
/>
|
||||
<Pagination />
|
||||
</Box>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{!!detailLogsId && (
|
||||
@@ -206,7 +220,7 @@ const Logs = ({ appId }: { appId: string }) => {
|
||||
>
|
||||
<ModalBody whiteSpace={'pre-wrap'}>{t('core.chat.Mark Description')}</ModalBody>
|
||||
</MyModal>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
191
projects/app/src/pages/app/detail/components/Plugin/Header.tsx
Normal file
191
projects/app/src/pages/app/detail/components/Plugin/Header.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
|
||||
import { useInterval } from 'ahooks';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import AppCard from '../WorkflowComponents/AppCard';
|
||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
|
||||
const {
|
||||
flowData2StoreDataAndCheck,
|
||||
onSaveWorkflow,
|
||||
setHistoriesDefaultData,
|
||||
historiesDefaultData,
|
||||
initData
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const onclickPublish = useCallback(async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
await onPublish({
|
||||
...data,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
}
|
||||
}, [flowData2StoreDataAndCheck, onPublish, appDetail.chatConfig]);
|
||||
|
||||
const saveAndBack = useCallback(async () => {
|
||||
try {
|
||||
await onSaveWorkflow();
|
||||
router.push('/app/list');
|
||||
} catch (error) {}
|
||||
}, [onSaveWorkflow, router]);
|
||||
// effect
|
||||
useBeforeunload({
|
||||
callback: onSaveWorkflow,
|
||||
tip: t('core.common.tip.leave page')
|
||||
});
|
||||
useInterval(() => {
|
||||
if (!appDetail._id) return;
|
||||
onSaveWorkflow();
|
||||
}, 40000);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{/* {!isPc && (
|
||||
<Flex pt={2} justifyContent={'center'}>
|
||||
<RouteTab />
|
||||
</Flex>
|
||||
)} */}
|
||||
<Flex
|
||||
mt={[2, 0]}
|
||||
py={3}
|
||||
pl={[2, 4]}
|
||||
pr={[2, 6]}
|
||||
borderBottom={'base'}
|
||||
alignItems={'center'}
|
||||
userSelect={'none'}
|
||||
h={'67px'}
|
||||
{...(currentTab === TabEnum.appEdit
|
||||
? {
|
||||
bg: 'myGray.25'
|
||||
}
|
||||
: {
|
||||
bg: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
})}
|
||||
>
|
||||
{/* back */}
|
||||
<MyIcon
|
||||
name={'common/leftArrowLight'}
|
||||
w={'1.75rem'}
|
||||
cursor={'pointer'}
|
||||
onClick={saveAndBack}
|
||||
/>
|
||||
{/* app info */}
|
||||
<Box ml={1}>
|
||||
<AppCard
|
||||
showSaveStatus={
|
||||
!historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* {isPc && (
|
||||
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
|
||||
<RouteTab />
|
||||
</Box>
|
||||
)} */}
|
||||
<Box flex={1} />
|
||||
|
||||
{currentTab === TabEnum.appEdit && (
|
||||
<>
|
||||
{!historiesDefaultData && (
|
||||
<IconButton
|
||||
// mr={[2, 4]}
|
||||
icon={<MyIcon name={'history'} w={'18px'} />}
|
||||
aria-label={''}
|
||||
size={'sm'}
|
||||
w={'30px'}
|
||||
variant={'whitePrimary'}
|
||||
onClick={async () => {
|
||||
const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore());
|
||||
|
||||
setHistoriesDefaultData({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* <Button
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
|
||||
variant={'whitePrimary'}
|
||||
onClick={async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
setWorkflowTestData(data);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('core.workflow.Debug')}
|
||||
</Button> */}
|
||||
|
||||
{!historiesDefaultData && (
|
||||
<PopoverConfirm
|
||||
showCancel
|
||||
content={t('core.app.Publish Confirm')}
|
||||
Trigger={
|
||||
<Button
|
||||
ml={[2, 4]}
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
|
||||
>
|
||||
{t('core.app.Publish')}
|
||||
</Button>
|
||||
}
|
||||
onConfirm={() => onclickPublish()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
{historiesDefaultData && (
|
||||
<PublishHistories
|
||||
initData={initData}
|
||||
onClose={() => {
|
||||
setHistoriesDefaultData(undefined);
|
||||
}}
|
||||
defaultData={historiesDefaultData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
appDetail.chatConfig,
|
||||
currentTab,
|
||||
historiesDefaultData,
|
||||
initData,
|
||||
isV2Workflow,
|
||||
onclickPublish,
|
||||
saveAndBack,
|
||||
setHistoriesDefaultData,
|
||||
t
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(Header);
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import { useMount } from 'ahooks';
|
||||
import Header from './Header';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { workflowBoxStyles } from '../constants';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import Flow from '../WorkflowComponents/Flow';
|
||||
const Logs = dynamic(() => import('../Logs/index'));
|
||||
const PublishChannel = dynamic(() => import('../Publish'));
|
||||
|
||||
const WorkflowEdit = () => {
|
||||
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
showCancel: false,
|
||||
content:
|
||||
'检测到您的高级编排为旧版,系统将为您自动格式化成新版工作流。\n\n由于版本差异较大,会导致一些工作流无法正常排布,请重新手动连接工作流。如仍异常,可尝试删除对应节点后重新添加。\n\n你可以直接点击调试进行工作流测试,调试完毕后点击发布。直到你点击发布,新工作流才会真正保存生效。\n\n在你发布新工作流前,自动保存不会生效。'
|
||||
});
|
||||
|
||||
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
|
||||
|
||||
useMount(() => {
|
||||
if (!isV2Workflow) {
|
||||
openConfirm(() => {
|
||||
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
|
||||
})();
|
||||
} else {
|
||||
initData(
|
||||
cloneDeep({
|
||||
nodes: appDetail.modules || [],
|
||||
edges: appDetail.edges || []
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex {...workflowBoxStyles}>
|
||||
<Header />
|
||||
|
||||
{currentTab === TabEnum.appEdit ? (
|
||||
<Flow />
|
||||
) : (
|
||||
<Flex flexDirection={'column'} h={'100%'} px={4} pb={4}>
|
||||
{currentTab === TabEnum.publish && <PublishChannel />}
|
||||
{currentTab === TabEnum.logs && <Logs />}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!isV2Workflow && <ConfirmModal countDown={0} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const Render = () => {
|
||||
return (
|
||||
<WorkflowContextProvider basicNodeTemplates={pluginSystemModuleTemplates}>
|
||||
<WorkflowEdit />
|
||||
</WorkflowContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
@@ -6,11 +6,7 @@ import { useI18n } from '@/web/context/I18n';
|
||||
|
||||
const API = ({ appId }: { appId: string }) => {
|
||||
const { publishT } = useI18n();
|
||||
return (
|
||||
<Box pt={3}>
|
||||
<ApiKeyTable tips={publishT('app key tips')} appId={appId} />
|
||||
</Box>
|
||||
);
|
||||
return <ApiKeyTable tips={publishT('app key tips')} appId={appId} />;
|
||||
};
|
||||
|
||||
export default API;
|
||||
|
||||
@@ -47,6 +47,7 @@ import { useI18n } from '@/web/context/I18n';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
|
||||
const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal'));
|
||||
|
||||
@@ -72,7 +73,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
|
||||
<MyBox h={'100%'} isLoading={isFetching} position={'relative'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<HStack>
|
||||
<Box color={'myGray.900'} fontSize={'lg'}>
|
||||
@@ -241,8 +242,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
|
||||
/>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
<Loading loading={isFetching} fixed={false} />
|
||||
</Box>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
|
||||
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -7,13 +7,20 @@ import dynamic from 'next/dynamic';
|
||||
import MyRadio from '@/components/common/MyRadio';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { cardStyles } from '../constants';
|
||||
|
||||
import Link from './Link';
|
||||
const API = dynamic(() => import('./API'));
|
||||
const FeiShu = dynamic(() => import('./FeiShu'));
|
||||
|
||||
const OutLink = ({ appId }: { appId: string }) => {
|
||||
const OutLink = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const appId = useContextSelector(AppContext, (v) => v.appId);
|
||||
|
||||
const publishList = useRef([
|
||||
{
|
||||
icon: '/imgs/modal/shareFill.svg',
|
||||
@@ -38,11 +45,8 @@ const OutLink = ({ appId }: { appId: string }) => {
|
||||
const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share);
|
||||
|
||||
return (
|
||||
<Box pt={[1, 5]}>
|
||||
<Box color={'myGray.900'} fontSize={'lg'} mb={2} px={[4, 8]}>
|
||||
{t('core.app.navbar.Publish app')}
|
||||
</Box>
|
||||
<Box pb={[5, 7]} px={[4, 8]} borderBottom={theme.borders.base}>
|
||||
<>
|
||||
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
|
||||
<MyRadio
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(auto-fill, minmax(0, 400px))']}
|
||||
iconSize={'20px'}
|
||||
@@ -52,12 +56,22 @@ const OutLink = ({ appId }: { appId: string }) => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{linkType === PublishChannelEnum.share && (
|
||||
<Link appId={appId} type={PublishChannelEnum.share} />
|
||||
)}
|
||||
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
|
||||
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
|
||||
</Box>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
{...cardStyles}
|
||||
boxShadow={3.5}
|
||||
mt={4}
|
||||
px={[4, 8]}
|
||||
py={[4, 6]}
|
||||
flex={'1 0 0'}
|
||||
>
|
||||
{linkType === PublishChannelEnum.share && (
|
||||
<Link appId={appId} type={PublishChannelEnum.share} />
|
||||
)}
|
||||
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
|
||||
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { getPublishList, postRevertVersion } from '@/web/core/app/versionApi';
|
||||
import { getPublishList, postRevertVersion } from '@/web/core/app/api/version';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -7,29 +7,38 @@ import { useMemoizedFn } from 'ahooks';
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { AppContext } from './context';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
|
||||
const PublishHistoriesSlider = () => {
|
||||
export type InitProps = {
|
||||
nodes: AppSchema['modules'];
|
||||
edges: AppSchema['edges'];
|
||||
chatConfig: AppSchema['chatConfig'];
|
||||
};
|
||||
|
||||
const PublishHistoriesSlider = ({
|
||||
onClose,
|
||||
initData,
|
||||
defaultData
|
||||
}: {
|
||||
onClose: () => void;
|
||||
initData: (data: InitProps) => void;
|
||||
defaultData: InitProps;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('core.workflow.publish.OnRevert version confirm')
|
||||
});
|
||||
|
||||
const { appDetail, setAppDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const appId = useContextSelector(WorkflowContext, (e) => e.appId);
|
||||
const setIsShowVersionHistories = useContextSelector(
|
||||
WorkflowContext,
|
||||
(e) => e.setIsShowVersionHistories
|
||||
);
|
||||
const initData = useContextSelector(WorkflowContext, (e) => e.initData);
|
||||
const appId = appDetail._id;
|
||||
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState<string>();
|
||||
|
||||
@@ -43,25 +52,25 @@ const PublishHistoriesSlider = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const onClose = useMemoizedFn(() => {
|
||||
setIsShowVersionHistories(false);
|
||||
});
|
||||
const onPreview = useCallback(
|
||||
(data: AppVersionSchemaType) => {
|
||||
setSelectedHistoryId(data._id);
|
||||
|
||||
const onPreview = useCallback((data: AppVersionSchemaType) => {
|
||||
setSelectedHistoryId(data._id);
|
||||
|
||||
initData({
|
||||
nodes: data.nodes,
|
||||
edges: data.edges
|
||||
});
|
||||
}, []);
|
||||
initData({
|
||||
nodes: data.nodes,
|
||||
edges: data.edges,
|
||||
chatConfig: data.chatConfig
|
||||
});
|
||||
},
|
||||
[initData]
|
||||
);
|
||||
const onCloseSlider = useCallback(
|
||||
(data: { nodes: StoreNodeItemType[]; edges: StoreEdgeItemType[] }) => {
|
||||
(data: InitProps) => {
|
||||
setSelectedHistoryId(undefined);
|
||||
initData(data);
|
||||
onClose();
|
||||
},
|
||||
[appDetail]
|
||||
[initData, onClose]
|
||||
);
|
||||
|
||||
const { mutate: onRevert, isLoading: isReverting } = useRequest({
|
||||
@@ -69,8 +78,9 @@ const PublishHistoriesSlider = () => {
|
||||
if (!appId) return;
|
||||
await postRevertVersion(appId, {
|
||||
versionId: data._id,
|
||||
editNodes: appDetail.modules, // old workflow
|
||||
editEdges: appDetail.edges
|
||||
editNodes: defaultData.nodes, // old workflow
|
||||
editEdges: defaultData.edges,
|
||||
editChatConfig: defaultData.chatConfig
|
||||
});
|
||||
|
||||
setAppDetail((state) => ({
|
||||
@@ -90,8 +100,9 @@ const PublishHistoriesSlider = () => {
|
||||
<CustomRightDrawer
|
||||
onClose={() =>
|
||||
onCloseSlider({
|
||||
nodes: appDetail.modules,
|
||||
edges: appDetail.edges
|
||||
nodes: defaultData.nodes,
|
||||
edges: defaultData.edges,
|
||||
chatConfig: defaultData.chatConfig
|
||||
})
|
||||
}
|
||||
iconSrc="core/workflow/versionHistories"
|
||||
@@ -99,7 +110,7 @@ const PublishHistoriesSlider = () => {
|
||||
maxW={'300px'}
|
||||
px={0}
|
||||
showMask={false}
|
||||
mt={'60px'}
|
||||
top={'60px'}
|
||||
overflow={'unset'}
|
||||
>
|
||||
<Button
|
||||
@@ -110,12 +121,13 @@ const PublishHistoriesSlider = () => {
|
||||
onClick={() => {
|
||||
setSelectedHistoryId(undefined);
|
||||
initData({
|
||||
nodes: appDetail.modules,
|
||||
edges: appDetail.edges
|
||||
nodes: defaultData.nodes,
|
||||
edges: defaultData.edges,
|
||||
chatConfig: defaultData.chatConfig
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('core.workflow.Current workflow')}
|
||||
{appT('Current settings')}
|
||||
</Button>
|
||||
<ScrollList isLoading={showLoading} flex={'1 0 0'} px={5}>
|
||||
{list.map((data, index) => {
|
||||
75
projects/app/src/pages/app/detail/components/RouteTab.tsx
Normal file
75
projects/app/src/pages/app/detail/components/RouteTab.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Box, HStack } from '@chakra-ui/react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { AppContext, TabEnum } from './context';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
const RouteTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
const router = useRouter();
|
||||
const { appDetail, currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const setCurrentTab = useCallback(
|
||||
(tab: TabEnum) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: tab
|
||||
}
|
||||
});
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const tabList = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: appDetail.type === AppTypeEnum.plugin ? appT('Setting plugin') : appT('Setting app'),
|
||||
id: TabEnum.appEdit
|
||||
},
|
||||
...(appDetail.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
label: appT('Publish channel'),
|
||||
id: TabEnum.publish
|
||||
},
|
||||
{ label: appT('Chat logs'), id: TabEnum.logs }
|
||||
]
|
||||
: [])
|
||||
],
|
||||
[appDetail.permission.hasManagePer, appT]
|
||||
);
|
||||
|
||||
return (
|
||||
<HStack spacing={4} whiteSpace={'nowrap'}>
|
||||
{tabList.map((tab) => (
|
||||
<Box
|
||||
key={tab.id}
|
||||
px={2}
|
||||
py={0.5}
|
||||
{...(currentTab === tab.id
|
||||
? {
|
||||
color: 'primary.700'
|
||||
}
|
||||
: {
|
||||
color: 'myGray.600',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: 'myGray.200',
|
||||
borderRadius: 'md'
|
||||
},
|
||||
onClick: () => setCurrentTab(tab.id)
|
||||
})}
|
||||
>
|
||||
{tab.label}
|
||||
</Box>
|
||||
))}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouteTab;
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
IconButton,
|
||||
HStack,
|
||||
Modal,
|
||||
ModalBody,
|
||||
Checkbox,
|
||||
ModalFooter
|
||||
} from '@chakra-ui/react';
|
||||
import { DragHandleIcon } from '@chakra-ui/icons';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import TagsEditModal from '../TagsEditModal';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import PermissionIconText from '@/components/support/permission/IconText';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postTransition2Workflow } from '@/web/core/app/api/app';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
const AppCard = () => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { appDetail, setAppDetail, onOpenInfoEdit, onDelApp } = useContextSelector(
|
||||
AppContext,
|
||||
(v) => v
|
||||
);
|
||||
const appId = appDetail._id;
|
||||
const { feConfigs } = useSystemStore();
|
||||
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
|
||||
|
||||
// transition to workflow
|
||||
const [transitionCreateNew, setTransitionCreateNew] = useState<boolean>();
|
||||
const { runAsync: onTransition, loading: transiting } = useRequest2(
|
||||
() => postTransition2Workflow({ appId, createNew: transitionCreateNew }),
|
||||
{
|
||||
onSuccess: ({ id }) => {
|
||||
if (id) {
|
||||
router.replace({
|
||||
query: {
|
||||
appId: id
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setAppDetail((state) => ({
|
||||
...state,
|
||||
type: AppTypeEnum.workflow
|
||||
}));
|
||||
}
|
||||
},
|
||||
successToast: t('common.Success')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* basic info */}
|
||||
<Box px={6} py={4} position={'relative'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={'md'} flex={'1 0 0'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box
|
||||
flex={1}
|
||||
mt={3}
|
||||
mb={4}
|
||||
className={'textEllipsis3'}
|
||||
wordBreak={'break-all'}
|
||||
color={'myGray.600'}
|
||||
fontSize={'xs'}
|
||||
minH={'46px'}
|
||||
>
|
||||
{appDetail.intro || t('core.app.tip.Add a intro to app')}
|
||||
</Box>
|
||||
<HStack>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
|
||||
onClick={() => router.push(`/chat?appId=${appId}`)}
|
||||
>
|
||||
{t('core.Chat')}
|
||||
</Button>
|
||||
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
|
||||
<Button
|
||||
mr={3}
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<DragHandleIcon w={'16px'} />}
|
||||
onClick={() => setTeamTagsSet(appDetail)}
|
||||
>
|
||||
{t('common.Team Tags Set')}
|
||||
</Button>
|
||||
)}
|
||||
{appDetail.permission.hasManagePer && (
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
|
||||
onClick={onOpenInfoEdit}
|
||||
>
|
||||
{t('common.Setting')}
|
||||
</Button>
|
||||
)}
|
||||
{appDetail.permission.isOwner && (
|
||||
<MyMenu
|
||||
Button={
|
||||
<IconButton
|
||||
variant={'whiteBase'}
|
||||
size={['smSquare', 'mdSquare']}
|
||||
icon={<MyIcon name={'more'} w={'1rem'} />}
|
||||
aria-label={''}
|
||||
/>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/app/type/workflow',
|
||||
label: appT('Transition to workflow'),
|
||||
onClick: () => setTransitionCreateNew(true)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'delete',
|
||||
type: 'danger',
|
||||
label: t('common.Delete'),
|
||||
onClick: onDelApp
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
<MyTag
|
||||
type="borderFill"
|
||||
colorSchema="gray"
|
||||
onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)}
|
||||
>
|
||||
<PermissionIconText defaultPermission={appDetail.defaultPermission} fontSize={'md'} />
|
||||
</MyTag>
|
||||
</HStack>
|
||||
</Box>
|
||||
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
|
||||
{transitionCreateNew !== undefined && (
|
||||
<MyModal isOpen title={appT('Transition to workflow')} iconSrc="core/app/type/workflow">
|
||||
<ModalBody>
|
||||
<Box mb={3}>{appT('Transition to workflow create new tip')}</Box>
|
||||
<HStack cursor={'pointer'} onClick={() => setTransitionCreateNew((state) => !state)}>
|
||||
<Checkbox isChecked={transitionCreateNew} />
|
||||
<Box>{appT('Transition to workflow create new placeholder')}</Box>
|
||||
</HStack>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} onClick={() => setTransitionCreateNew(undefined)} mr={3}>
|
||||
{t('common.Close')}
|
||||
</Button>
|
||||
<Button variant={'dangerFill'} isLoading={transiting} onClick={() => onTransition()}>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AppCard);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useEffect } from 'react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
import { useSafeState } from 'ahooks';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { useChatTest } from '../useChatTest';
|
||||
|
||||
const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const [workflowData, setWorkflowData] = useSafeState({
|
||||
nodes: appDetail.modules || [],
|
||||
edges: appDetail.edges || []
|
||||
});
|
||||
useEffect(() => {
|
||||
const { nodes, edges } = form2AppWorkflow(appForm);
|
||||
setWorkflowData({ nodes, edges });
|
||||
}, [appForm, setWorkflowData]);
|
||||
|
||||
const { resetChatBox, ChatBox } = useChatTest({
|
||||
...workflowData,
|
||||
chatConfig: appForm.chatConfig
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex position={'relative'} flexDirection={'column'} h={'100%'} py={4}>
|
||||
<Flex px={[2, 5]}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1}>
|
||||
{appT('Chat Debug')}
|
||||
</Box>
|
||||
<MyTooltip label={t('core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
resetChatBox();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatBox />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ChatTest);
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useMount } from 'ahooks';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
||||
|
||||
import ChatTest from './ChatTest';
|
||||
import AppCard from './AppCard';
|
||||
import EditForm from './EditForm';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { cardStyles } from '../constants';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
const Edit = ({
|
||||
appForm,
|
||||
setAppForm
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
}) => {
|
||||
const { isPc } = useSystemStore();
|
||||
const { loadAllDatasets } = useDatasetStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
// show selected dataset
|
||||
useMount(() => {
|
||||
loadAllDatasets();
|
||||
|
||||
setAppForm(
|
||||
appWorkflow2Form({
|
||||
nodes: appDetail.modules,
|
||||
chatConfig: appDetail.chatConfig
|
||||
})
|
||||
);
|
||||
|
||||
if (appDetail.version !== 'v2') {
|
||||
setAppForm(
|
||||
appWorkflow2Form({
|
||||
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
|
||||
chatConfig: appDetail.chatConfig
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={['block', 'flex']}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
pt={[2, 1.5]}
|
||||
pl={[2, 1]}
|
||||
gap={1}
|
||||
borderRadius={'lg'}
|
||||
overflowY={['auto', 'unset']}
|
||||
>
|
||||
<Box className={styles.EditAppBox} pr={[0, 1]} overflowY={'auto'} minW={'580px'} flex={'1'}>
|
||||
<Box {...cardStyles} boxShadow={'2'}>
|
||||
<AppCard />
|
||||
</Box>
|
||||
|
||||
<Box mt={4} {...cardStyles} boxShadow={'3.5'} w={'auto'}>
|
||||
<EditForm appForm={appForm} setAppForm={setAppForm} />
|
||||
</Box>
|
||||
</Box>
|
||||
{isPc && (
|
||||
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0}>
|
||||
<ChatTest appForm={appForm} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Edit);
|
||||
@@ -0,0 +1,480 @@
|
||||
import React, { useEffect, useMemo, useTransition } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Grid,
|
||||
BoxProps,
|
||||
useTheme,
|
||||
useDisclosure,
|
||||
Button,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import { AddIcon, SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { useFieldArray, UseFormReturn } from 'react-hook-form';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import VariableEdit from '@/components/core/app/VariableEdit';
|
||||
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
|
||||
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
|
||||
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
|
||||
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
|
||||
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
|
||||
import { TTSTypeEnum } from '@/web/core/app/constants';
|
||||
import { getSystemVariables } from '@/web/core/app/utils';
|
||||
import { useUpdate } from 'ahooks';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
|
||||
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
|
||||
const ToolSelectModal = dynamic(() => import('./components/ToolSelectModal'));
|
||||
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
|
||||
const QGSwitch = dynamic(() => import('@/components/core/app/QGSwitch'));
|
||||
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
|
||||
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
|
||||
const ScheduledTriggerConfig = dynamic(
|
||||
() => import('@/components/core/app/ScheduledTriggerConfig')
|
||||
);
|
||||
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
|
||||
|
||||
const BoxStyles: BoxProps = {
|
||||
px: 5,
|
||||
py: '16px',
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'borderColor.low'
|
||||
};
|
||||
const LabelStyles: BoxProps = {
|
||||
w: ['60px', '100px'],
|
||||
flexShrink: 0,
|
||||
fontSize: 'xs'
|
||||
};
|
||||
|
||||
const EditForm = ({
|
||||
appForm,
|
||||
setAppForm
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const { allDatasets } = useDatasetStore();
|
||||
const { llmModelList } = useSystemStore();
|
||||
const [, startTst] = useTransition();
|
||||
|
||||
const selectDatasets = useMemo(
|
||||
() =>
|
||||
allDatasets.filter((item) =>
|
||||
appForm.dataset?.datasets.find((dataset) => dataset.datasetId === item._id)
|
||||
),
|
||||
[allDatasets, appForm?.dataset?.datasets]
|
||||
);
|
||||
|
||||
const {
|
||||
isOpen: isOpenDatasetSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenDatasetParams,
|
||||
onOpen: onOpenDatasetParams,
|
||||
onClose: onCloseDatasetParams
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenToolsSelect,
|
||||
onOpen: onOpenToolsSelect,
|
||||
onClose: onCloseToolsSelect
|
||||
} = useDisclosure();
|
||||
|
||||
const formatVariables: any = useMemo(
|
||||
() =>
|
||||
formatEditorVariablePickerIcon([
|
||||
...getSystemVariables(t),
|
||||
...(appForm.chatConfig.variables || [])
|
||||
]),
|
||||
[appForm.chatConfig.variables, t]
|
||||
);
|
||||
|
||||
const tokenLimit = useMemo(() => {
|
||||
return (
|
||||
llmModelList.find((item) => item.model === appForm.aiSettings.model)?.quoteMaxToken || 3000
|
||||
);
|
||||
}, [llmModelList, appForm.aiSettings.model]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
{/* ai */}
|
||||
<Box {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
|
||||
<FormLabel ml={2} flex={1}>
|
||||
{appT('AI Settings')}
|
||||
</FormLabel>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box {...LabelStyles}>{t('core.ai.Model')}</Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<SettingLLMModel
|
||||
llmModelType={'all'}
|
||||
defaultData={{
|
||||
model: appForm.aiSettings.model,
|
||||
temperature: appForm.aiSettings.temperature,
|
||||
maxToken: appForm.aiSettings.maxToken,
|
||||
maxHistories: appForm.aiSettings.maxHistories
|
||||
}}
|
||||
onChange={({ model, temperature, maxToken, maxHistories }: SettingAIDataType) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
aiSettings: {
|
||||
...state.aiSettings,
|
||||
model,
|
||||
temperature,
|
||||
maxToken,
|
||||
maxHistories: maxHistories ?? 6
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Box mt={3}>
|
||||
<HStack {...LabelStyles}>
|
||||
<Box>{t('core.ai.Prompt')}</Box>
|
||||
<QuestionTip label={t('core.app.tip.chatNodeSystemPromptTip')} />
|
||||
</HStack>
|
||||
<Box mt={1}>
|
||||
<PromptEditor
|
||||
value={appForm.aiSettings.systemPrompt}
|
||||
onChange={(text) => {
|
||||
startTst(() => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
aiSettings: {
|
||||
...state.aiSettings,
|
||||
systemPrompt: text
|
||||
}
|
||||
}));
|
||||
});
|
||||
}}
|
||||
variables={formatVariables}
|
||||
placeholder={t('core.app.tip.chatNodeSystemPromptTip')}
|
||||
title={t('core.ai.Prompt')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* dataset */}
|
||||
<Box {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'} flex={1}>
|
||||
<MyIcon name={'core/app/simpleMode/dataset'} w={'20px'} />
|
||||
<FormLabel ml={2}>{t('core.dataset.Choose Dataset')}</FormLabel>
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'transparentBase'}
|
||||
leftIcon={<MyIcon name="common/addLight" w={'0.8rem'} />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
fontSize={'sm'}
|
||||
onClick={onOpenKbSelect}
|
||||
>
|
||||
{t('common.Choose')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'transparentBase'}
|
||||
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
fontSize={'sm'}
|
||||
onClick={onOpenDatasetParams}
|
||||
>
|
||||
{t('common.Params')}
|
||||
</Button>
|
||||
</Flex>
|
||||
{appForm.dataset.datasets?.length > 0 && (
|
||||
<Box my={3}>
|
||||
<SearchParamsTip
|
||||
searchMode={appForm.dataset.searchMode}
|
||||
similarity={appForm.dataset.similarity}
|
||||
limit={appForm.dataset.limit}
|
||||
usingReRank={appForm.dataset.usingReRank}
|
||||
queryExtensionModel={appForm.dataset.datasetSearchExtensionModel}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
|
||||
{selectDatasets.map((item) => (
|
||||
<MyTooltip key={item._id} label={t('core.dataset.Read Dataset')}>
|
||||
<Flex
|
||||
overflow={'hidden'}
|
||||
alignItems={'center'}
|
||||
p={2}
|
||||
bg={'white'}
|
||||
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
|
||||
borderRadius={'md'}
|
||||
border={theme.borders.base}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
router.push({
|
||||
pathname: '/dataset/detail',
|
||||
query: {
|
||||
datasetId: item._id
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'18px'} mr={1} />
|
||||
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* tool choice */}
|
||||
<Box {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'} flex={1}>
|
||||
<MyIcon name={'core/app/toolCall'} w={'20px'} />
|
||||
<FormLabel ml={2}>{t('core.app.Tool call')}(实验功能)</FormLabel>
|
||||
<QuestionTip ml={1} label={t('core.app.Tool call tip')} />
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'transparentBase'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
mr={'-5px'}
|
||||
size={'sm'}
|
||||
fontSize={'sm'}
|
||||
onClick={onOpenToolsSelect}
|
||||
>
|
||||
{t('common.Choose')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Grid
|
||||
mt={appForm.selectedTools.length > 0 ? 2 : 0}
|
||||
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
|
||||
gridGap={[2, 4]}
|
||||
>
|
||||
{appForm.selectedTools.map((item) => (
|
||||
<MyTooltip key={item.id} label={item.intro}>
|
||||
<Flex
|
||||
overflow={'hidden'}
|
||||
alignItems={'center'}
|
||||
p={2.5}
|
||||
bg={'white'}
|
||||
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
|
||||
borderRadius={'md'}
|
||||
border={theme.borders.base}
|
||||
_hover={{
|
||||
...hoverDeleteStyles,
|
||||
borderColor: 'primary.300'
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'1rem'} mr={1} />
|
||||
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<DeleteIcon
|
||||
onClick={() => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
selectedTools: state.selectedTools.filter((tool) => tool.id !== item.id)
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* variable */}
|
||||
<Box {...BoxStyles}>
|
||||
<VariableEdit
|
||||
variables={appForm.chatConfig.variables}
|
||||
onChange={(e) => {
|
||||
appForm.chatConfig.variables = e;
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* welcome */}
|
||||
<Box {...BoxStyles}>
|
||||
<WelcomeTextConfig
|
||||
value={appForm.chatConfig.welcomeText}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
welcomeText: e.target.value
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* tts */}
|
||||
<Box {...BoxStyles}>
|
||||
<TTSSelect
|
||||
value={appForm.chatConfig.ttsConfig}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
ttsConfig: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* whisper */}
|
||||
<Box {...BoxStyles}>
|
||||
<WhisperConfig
|
||||
isOpenAudio={appForm.chatConfig.ttsConfig?.type !== TTSTypeEnum.none}
|
||||
value={appForm.chatConfig.whisperConfig}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
whisperConfig: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* question guide */}
|
||||
<Box {...BoxStyles}>
|
||||
<QGSwitch
|
||||
isChecked={appForm.chatConfig.questionGuide}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
questionGuide: e.target.checked
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* question tips */}
|
||||
<Box {...BoxStyles}>
|
||||
<InputGuideConfig
|
||||
appId={appDetail._id}
|
||||
value={appForm.chatConfig.chatInputGuide}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
chatInputGuide: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* timer trigger */}
|
||||
<Box {...BoxStyles} borderBottom={'none'}>
|
||||
<ScheduledTriggerConfig
|
||||
value={appForm.chatConfig.scheduledTriggerConfig}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
chatConfig: {
|
||||
...state.chatConfig,
|
||||
scheduledTriggerConfig: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isOpenDatasetSelect && (
|
||||
<DatasetSelectModal
|
||||
isOpen={isOpenDatasetSelect}
|
||||
defaultSelectedDatasets={selectDatasets.map((item) => ({
|
||||
datasetId: item._id,
|
||||
vectorModel: item.vectorModel
|
||||
}))}
|
||||
onClose={onCloseKbSelect}
|
||||
onChange={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
dataset: {
|
||||
...state.dataset,
|
||||
datasets: e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOpenDatasetParams && (
|
||||
<DatasetParamsModal
|
||||
{...appForm.dataset}
|
||||
maxTokens={tokenLimit}
|
||||
onClose={onCloseDatasetParams}
|
||||
onSuccess={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
dataset: {
|
||||
...state.dataset,
|
||||
...e
|
||||
}
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOpenToolsSelect && (
|
||||
<ToolSelectModal
|
||||
selectedTools={appForm.selectedTools}
|
||||
onAddTool={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
selectedTools: [...state.selectedTools, e]
|
||||
}));
|
||||
}}
|
||||
onRemoveTool={(e) => {
|
||||
setAppForm((state) => ({
|
||||
...state,
|
||||
selectedTools: state.selectedTools.filter((item) => item.id !== e.id)
|
||||
}));
|
||||
}}
|
||||
onClose={onCloseToolsSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EditForm);
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import RouteTab from '../RouteTab';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { TabEnum } from '../context';
|
||||
import PublishHistoriesSlider, { type InitProps } from '../PublishHistoriesSlider';
|
||||
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { compareWorkflow } from '@/web/core/workflow/utils';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { publishStatusStyle } from '../constants';
|
||||
|
||||
const Header = ({
|
||||
appForm,
|
||||
setAppForm
|
||||
}: {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystemStore();
|
||||
const router = useRouter();
|
||||
const { appId, appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), {
|
||||
manual: false,
|
||||
refreshDeps: [appId]
|
||||
});
|
||||
const onclickRoute = useCallback(
|
||||
(parentId: string) => {
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
query: {
|
||||
parentId
|
||||
}
|
||||
});
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const isPublished = useMemo(() => {
|
||||
const data = form2AppWorkflow(appForm);
|
||||
|
||||
return compareWorkflow(
|
||||
{
|
||||
nodes: appDetail.modules,
|
||||
edges: [],
|
||||
chatConfig: appDetail.chatConfig
|
||||
},
|
||||
{
|
||||
nodes: data.nodes,
|
||||
edges: [],
|
||||
chatConfig: data.chatConfig
|
||||
}
|
||||
);
|
||||
}, [appDetail.chatConfig, appDetail.modules, appForm]);
|
||||
|
||||
const onSubmitPublish = useCallback(
|
||||
async (data: AppSimpleEditFormType) => {
|
||||
const { nodes, edges } = form2AppWorkflow(data);
|
||||
await onPublish({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: data.chatConfig,
|
||||
type: AppTypeEnum.simple
|
||||
});
|
||||
},
|
||||
[onPublish]
|
||||
);
|
||||
|
||||
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{!isPc && (
|
||||
<Flex pt={2} justifyContent={'center'}>
|
||||
<RouteTab />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex pl={2} pt={[2, 3]} alignItems={'flex-start'} position={'relative'}>
|
||||
<Box flex={'1'}>
|
||||
<FolderPath paths={paths} hoverStyle={{ color: 'primary.600' }} onClick={onclickRoute} />
|
||||
</Box>
|
||||
{isPc && (
|
||||
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
|
||||
<RouteTab />
|
||||
</Box>
|
||||
)}
|
||||
{currentTab === TabEnum.appEdit && (
|
||||
<Flex alignItems={'center'}>
|
||||
{!historiesDefaultData && (
|
||||
<>
|
||||
<MyTag
|
||||
mr={3}
|
||||
type={'borderFill'}
|
||||
showDot
|
||||
colorSchema={
|
||||
isPublished
|
||||
? publishStatusStyle.published.colorSchema
|
||||
: publishStatusStyle.unPublish.colorSchema
|
||||
}
|
||||
>
|
||||
{isPublished
|
||||
? publishStatusStyle.published.text
|
||||
: publishStatusStyle.unPublish.text}
|
||||
</MyTag>
|
||||
<IconButton
|
||||
mr={[2, 4]}
|
||||
icon={<MyIcon name={'history'} w={'18px'} />}
|
||||
aria-label={''}
|
||||
size={'sm'}
|
||||
w={'30px'}
|
||||
variant={'whitePrimary'}
|
||||
onClick={() => {
|
||||
const { nodes, edges } = form2AppWorkflow(appForm);
|
||||
setHistoriesDefaultData({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: appForm.chatConfig
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<PopoverConfirm
|
||||
showCancel
|
||||
content={t('core.app.Publish Confirm')}
|
||||
Trigger={<Button isDisabled={isPublished}>{t('core.app.Publish')}</Button>}
|
||||
onConfirm={() => onSubmitPublish(appForm)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{!!historiesDefaultData && (
|
||||
<PublishHistoriesSlider
|
||||
initData={({ nodes, chatConfig }) => {
|
||||
setAppForm(
|
||||
appWorkflow2Form({
|
||||
nodes,
|
||||
chatConfig
|
||||
})
|
||||
);
|
||||
}}
|
||||
onClose={() => setHistoriesDefaultData(undefined)}
|
||||
defaultData={historiesDefaultData}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -20,24 +20,25 @@ import {
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
|
||||
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { getPreviewPluginModule } from '@/web/core/plugin/api';
|
||||
import { getPreviewPluginNode, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import ParentPaths from '@/components/common/ParentPaths';
|
||||
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
import { debounce } from 'lodash';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
|
||||
type Props = {
|
||||
selectedTools: FlowNodeTemplateType[];
|
||||
@@ -52,55 +53,38 @@ enum TemplateTypeEnum {
|
||||
|
||||
const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
systemNodeTemplates,
|
||||
loadSystemNodeTemplates,
|
||||
teamPluginNodeTemplates,
|
||||
loadTeamPluginNodeTemplates
|
||||
} = useWorkflowStore();
|
||||
|
||||
const [templateType, setTemplateType] = useState(TemplateTypeEnum.teamPlugin);
|
||||
const [currentParent, setCurrentParent] = useState<{
|
||||
parentId: string;
|
||||
parentName: string;
|
||||
}>();
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const templates = useMemo(() => {
|
||||
const map = {
|
||||
[TemplateTypeEnum.systemPlugin]: systemNodeTemplates.filter(
|
||||
(item) => item.isTool && item.name.toLowerCase().includes(searchKey.toLowerCase())
|
||||
),
|
||||
[TemplateTypeEnum.teamPlugin]: teamPluginNodeTemplates.filter((item) =>
|
||||
searchKey ? item.pluginType !== PluginTypeEnum.folder : true
|
||||
)
|
||||
};
|
||||
return map[templateType];
|
||||
}, [searchKey, systemNodeTemplates, teamPluginNodeTemplates, templateType]);
|
||||
|
||||
const { mutate: onChangeTab } = useRequest({
|
||||
mutationFn: async (e: any) => {
|
||||
const val = e as TemplateTypeEnum;
|
||||
if (val === TemplateTypeEnum.systemPlugin) {
|
||||
await loadSystemNodeTemplates();
|
||||
} else if (val === TemplateTypeEnum.teamPlugin) {
|
||||
await loadTeamPluginNodeTemplates({
|
||||
parentId: currentParent?.parentId
|
||||
const { data: templates = [], loading: isLoading } = useRequest2(
|
||||
async () => {
|
||||
if (templateType === TemplateTypeEnum.systemPlugin) {
|
||||
return (await getSystemPlugTemplates()).filter(
|
||||
(item) => item.isTool && item.name.toLowerCase().includes(searchKey.toLowerCase())
|
||||
);
|
||||
} else if (templateType === TemplateTypeEnum.teamPlugin) {
|
||||
return getTeamPlugTemplates({
|
||||
parentId,
|
||||
searchKey,
|
||||
type: [AppTypeEnum.folder, AppTypeEnum.httpPlugin, AppTypeEnum.plugin]
|
||||
});
|
||||
}
|
||||
setTemplateType(val);
|
||||
},
|
||||
errorToast: t('core.module.templates.Load plugin error')
|
||||
});
|
||||
|
||||
const { isLoading } = useQuery(['teamNodeTemplate', currentParent?.parentId, searchKey], () =>
|
||||
loadTeamPluginNodeTemplates({
|
||||
parentId: currentParent?.parentId,
|
||||
searchKey,
|
||||
init: true
|
||||
})
|
||||
{
|
||||
manual: false,
|
||||
throttleWait: 300,
|
||||
refreshDeps: [templateType, searchKey, parentId],
|
||||
errorToast: t('core.module.templates.Load plugin error')
|
||||
}
|
||||
);
|
||||
|
||||
const { data: paths = [] } = useRequest2(() => getAppFolderPath(parentId), {
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
@@ -129,7 +113,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
|
||||
py={'5px'}
|
||||
px={'15px'}
|
||||
value={templateType}
|
||||
onChange={onChangeTab}
|
||||
onChange={(e) => setTemplateType(e as TemplateTypeEnum)}
|
||||
/>
|
||||
<InputGroup w={300}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
@@ -138,20 +122,19 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('plugin.Search plugin')}
|
||||
onChange={debounce((e) => setSearchKey(e.target.value), 200)}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Box>
|
||||
{/* route components */}
|
||||
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && currentParent && (
|
||||
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && parentId && (
|
||||
<Flex mt={2} px={[3, 6]}>
|
||||
<ParentPaths
|
||||
paths={[currentParent]}
|
||||
<FolderPath
|
||||
paths={paths}
|
||||
FirstPathDom={null}
|
||||
onClick={() => {
|
||||
setCurrentParent(undefined);
|
||||
setParentId(null);
|
||||
}}
|
||||
fontSize="md"
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -159,7 +142,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
|
||||
<RenderList
|
||||
templates={templates}
|
||||
isLoadingData={isLoading}
|
||||
setCurrentParent={setCurrentParent}
|
||||
setParentId={setParentId}
|
||||
{...props}
|
||||
/>
|
||||
</MyBox>
|
||||
@@ -175,11 +158,11 @@ const RenderList = React.memo(function RenderList({
|
||||
isLoadingData,
|
||||
onAddTool,
|
||||
onRemoveTool,
|
||||
setCurrentParent
|
||||
setParentId
|
||||
}: Props & {
|
||||
templates: FlowNodeTemplateType[];
|
||||
isLoadingData: boolean;
|
||||
setCurrentParent: (e: { parentId: string; parentName: string }) => void;
|
||||
setParentId: React.Dispatch<React.SetStateAction<ParentIdType>>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [configTool, setConfigTool] = useState<FlowNodeTemplateType>();
|
||||
@@ -204,7 +187,7 @@ const RenderList = React.memo(function RenderList({
|
||||
|
||||
const { mutate: onClickAdd, isLoading } = useRequest({
|
||||
mutationFn: async (template: FlowNodeTemplateType) => {
|
||||
const res = await getPreviewPluginModule(template.id);
|
||||
const res = await getPreviewPluginNode({ appId: template.id });
|
||||
|
||||
if (!checkToolInputValid(res)) {
|
||||
return Promise.reject(t('core.app.ToolCall.This plugin cannot be called as a tool'));
|
||||
@@ -250,7 +233,7 @@ const RenderList = React.memo(function RenderList({
|
||||
<Box ml={5} flex={'1 0 0'}>
|
||||
<Box color={'black'}>{t(item.name)}</Box>
|
||||
{item.intro && (
|
||||
<Box className="textEllipsis3" color={'myGray.500'} fontSize={['xs', 'sm']}>
|
||||
<Box className="textEllipsis3" color={'myGray.500'} fontSize={'xs'}>
|
||||
{t(item.intro)}
|
||||
</Box>
|
||||
)}
|
||||
@@ -264,12 +247,9 @@ const RenderList = React.memo(function RenderList({
|
||||
>
|
||||
{t('common.Remove')}
|
||||
</Button>
|
||||
) : item.pluginType === PluginTypeEnum.folder ? (
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whiteBase'}
|
||||
onClick={() => setCurrentParent({ parentId: item.id, parentName: item.name })}
|
||||
>
|
||||
) : item.pluginType === PluginTypeEnum.folder ||
|
||||
item.pluginType === AppTypeEnum.httpPlugin ? (
|
||||
<Button size={'sm'} variant={'whiteBase'} onClick={() => setParentId(item.id)}>
|
||||
{t('common.Open')}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
|
||||
|
||||
import Header from './Header';
|
||||
import Edit from './Edit';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
const Logs = dynamic(() => import('../Logs/index'));
|
||||
const PublishChannel = dynamic(() => import('../Publish'));
|
||||
|
||||
const SimpleEdit = () => {
|
||||
const { currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const [appForm, setAppForm] = useState(getDefaultAppForm());
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} pr={3} pb={3}>
|
||||
<Header appForm={appForm} setAppForm={setAppForm} />
|
||||
{currentTab === TabEnum.appEdit ? (
|
||||
<Edit appForm={appForm} setAppForm={setAppForm} />
|
||||
) : (
|
||||
<Flex h={'100%'} flexDirection={'column'} mt={4}>
|
||||
{currentTab === TabEnum.publish && <PublishChannel />}
|
||||
{currentTab === TabEnum.logs && <Logs />}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SimpleEdit);
|
||||
@@ -0,0 +1,10 @@
|
||||
.EditAppBox {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #dfe2ea !important;
|
||||
transition: background 1s;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--chakra-colors-gray-300) !important;
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Flex, Button, IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { DragHandleIcon } from '@chakra-ui/icons';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type.d';
|
||||
import { delAppById } from '@/web/core/app/api';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import PermissionIconText from '@/components/support/permission/IconText';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import TagsEditModal from './TagsEditModal';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
const InfoModal = dynamic(() => import('../InfoModal'));
|
||||
|
||||
const AppCard = () => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const appId = appDetail._id;
|
||||
const { feConfigs } = useSystemStore();
|
||||
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
|
||||
|
||||
const {
|
||||
isOpen: isOpenInfoEdit,
|
||||
onOpen: onOpenInfoEdit,
|
||||
onClose: onCloseInfoEdit
|
||||
} = useDisclosure();
|
||||
|
||||
const { openConfirm: openConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({
|
||||
content: appT('Confirm Del App Tip'),
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
/* 点击删除 */
|
||||
const { mutate: handleDelModel, isLoading } = useRequest({
|
||||
mutationFn: async () => {
|
||||
if (!appDetail) return null;
|
||||
await delAppById(appDetail._id);
|
||||
return 'success';
|
||||
},
|
||||
onSuccess(res) {
|
||||
if (!res) return;
|
||||
toast({
|
||||
title: t('common.Delete Success'),
|
||||
status: 'success'
|
||||
});
|
||||
router.replace(`/app/list`);
|
||||
},
|
||||
errorToast: t('common.Delete Failed')
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box px={4}>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'}>
|
||||
<PermissionIconText defaultPermission={appDetail.defaultPermission} fontSize={'md'} />
|
||||
</Box>
|
||||
<Box color={'myGray.500'} fontSize={'xs'}>
|
||||
AppId:{' '}
|
||||
<Box as={'span'} userSelect={'all'}>
|
||||
{appId}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
{/* basic info */}
|
||||
<Box
|
||||
borderWidth={'1px'}
|
||||
borderColor={'primary.1'}
|
||||
borderRadius={'md'}
|
||||
mt={2}
|
||||
px={5}
|
||||
py={4}
|
||||
bg={'primary.50'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={'md'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
{appDetail.permission.isOwner && (
|
||||
<IconButton
|
||||
className="delete"
|
||||
position={'absolute'}
|
||||
top={4}
|
||||
right={4}
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
isLoading={isLoading}
|
||||
onClick={openConfirmDel(handleDelModel)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Box
|
||||
flex={1}
|
||||
mt={3}
|
||||
mb={4}
|
||||
className={'textEllipsis3'}
|
||||
wordBreak={'break-all'}
|
||||
color={'myGray.600'}
|
||||
fontSize={'xs'}
|
||||
>
|
||||
{appDetail.intro || t('core.app.tip.Add a intro to app')}
|
||||
</Box>
|
||||
<Flex>
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
|
||||
onClick={() => router.push(`/chat?appId=${appId}`)}
|
||||
>
|
||||
{t('core.Chat')}
|
||||
</Button>
|
||||
<Button
|
||||
mx={3}
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<MyIcon name={'support/outlink/shareLight'} w={'16px'} />}
|
||||
onClick={() => {
|
||||
router.replace({
|
||||
query: {
|
||||
appId,
|
||||
currentTab: 'publish'
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('core.app.navbar.Publish')}
|
||||
</Button>
|
||||
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
|
||||
<Button
|
||||
mr={3}
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<DragHandleIcon w={'16px'} />}
|
||||
onClick={() => setTeamTagsSet(appDetail)}
|
||||
>
|
||||
{t('common.Team Tags Set')}
|
||||
</Button>
|
||||
)}
|
||||
{appDetail.permission.hasManagePer && (
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
|
||||
onClick={onOpenInfoEdit}
|
||||
>
|
||||
{t('common.Setting')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<ConfirmDelModal />
|
||||
{isOpenInfoEdit && <InfoModal onClose={onCloseInfoEdit} />}
|
||||
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AppCard);
|
||||
@@ -1,160 +0,0 @@
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import ChatBox from '@/components/ChatBox';
|
||||
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
|
||||
import { streamFetch } from '@/web/common/api/fetch';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { checkChatSupportSelectFileByModules } from '@/web/core/chat/utils';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import {
|
||||
getDefaultEntryNodeIds,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
initWorkflowEdgeStatus,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { useMemoizedFn, useSafeState } from 'ahooks';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
|
||||
const ChatTest = ({
|
||||
editForm,
|
||||
appId
|
||||
}: {
|
||||
editForm: UseFormReturn<AppSimpleEditFormType, any>;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { userInfo } = useUserStore();
|
||||
const ChatBoxRef = useRef<ComponentRef>(null);
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const { watch } = editForm;
|
||||
const chatConfig = watch('chatConfig');
|
||||
|
||||
const [workflowData, setWorkflowData] = useSafeState({
|
||||
nodes: appDetail.modules || [],
|
||||
edges: appDetail.edges || []
|
||||
});
|
||||
|
||||
const startChat = useMemoizedFn(
|
||||
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
|
||||
if (!workflowData) return Promise.reject('workflowData is empty');
|
||||
|
||||
/* get histories */
|
||||
let historyMaxLen = getMaxHistoryLimitFromNodes(workflowData.nodes);
|
||||
|
||||
const history = chatList.slice(-historyMaxLen - 2, -2);
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText, responseData } = await streamFetch({
|
||||
url: '/api/core/chat/chatTest',
|
||||
data: {
|
||||
history,
|
||||
prompt: chatList[chatList.length - 2].value,
|
||||
nodes: storeNodes2RuntimeNodes(
|
||||
workflowData.nodes,
|
||||
getDefaultEntryNodeIds(workflowData.nodes)
|
||||
),
|
||||
edges: initWorkflowEdgeStatus(workflowData.edges),
|
||||
variables,
|
||||
appId,
|
||||
appName: `调试-${appDetail.name}`
|
||||
},
|
||||
onMessage: generatingMessage,
|
||||
abortCtrl: controller
|
||||
});
|
||||
|
||||
return { responseText, responseData };
|
||||
}
|
||||
);
|
||||
|
||||
const resetChatBox = useCallback(() => {
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const wat = watch((data) => {
|
||||
const { nodes, edges } = form2AppWorkflow(data as AppSimpleEditFormType);
|
||||
setWorkflowData({ nodes, edges });
|
||||
});
|
||||
|
||||
return () => {
|
||||
wat.unsubscribe();
|
||||
};
|
||||
}, [setWorkflowData, watch]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
py={4}
|
||||
overflowX={'auto'}
|
||||
bg={'white'}
|
||||
>
|
||||
<Flex px={[2, 5]}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1}>
|
||||
{appT('Chat Debug')}
|
||||
</Box>
|
||||
<MyTooltip label={t('core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
resetChatBox();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
appId={appDetail._id}
|
||||
appAvatar={appDetail.avatar}
|
||||
userAvatar={userInfo?.avatar}
|
||||
showMarkIcon
|
||||
chatConfig={chatConfig}
|
||||
showFileSelector={checkChatSupportSelectFileByModules(workflowData.nodes)}
|
||||
onStartChat={startChat}
|
||||
onDelMessage={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
{appDetail.type !== AppTypeEnum.simple && (
|
||||
<Flex
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
right={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
bg={'rgba(255,255,255,0.7)'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
flexDirection={'column'}
|
||||
fontSize={'lg'}
|
||||
color={'black'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
textAlign={'center'}
|
||||
>
|
||||
<Box fontSize={'md'}>{appT('Advance App TestTip')}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ChatTest);
|
||||
@@ -1,521 +0,0 @@
|
||||
import React, { useEffect, useMemo, useTransition } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Grid,
|
||||
BoxProps,
|
||||
useTheme,
|
||||
useDisclosure,
|
||||
Button,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import { AddIcon, SmallAddIcon } from '@chakra-ui/icons';
|
||||
import { useFieldArray, UseFormReturn } from 'react-hook-form';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import VariableEdit from '@/components/core/app/VariableEdit';
|
||||
import MyTextarea from '@/components/common/Textarea/MyTextarea/index';
|
||||
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
|
||||
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
|
||||
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
|
||||
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
|
||||
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
|
||||
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
|
||||
import { TTSTypeEnum } from '@/web/core/app/constants';
|
||||
import { getSystemVariables } from '@/web/core/app/utils';
|
||||
import { useUpdate } from 'ahooks';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
|
||||
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
|
||||
const ToolSelectModal = dynamic(() => import('./ToolSelectModal'));
|
||||
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
|
||||
const QGSwitch = dynamic(() => import('@/components/core/app/QGSwitch'));
|
||||
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
|
||||
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
|
||||
const ScheduledTriggerConfig = dynamic(
|
||||
() => import('@/components/core/app/ScheduledTriggerConfig')
|
||||
);
|
||||
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
|
||||
|
||||
const BoxStyles: BoxProps = {
|
||||
px: 5,
|
||||
py: '16px',
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'borderColor.low'
|
||||
};
|
||||
const LabelStyles: BoxProps = {
|
||||
w: ['60px', '100px'],
|
||||
flexShrink: 0,
|
||||
fontSize: 'xs'
|
||||
};
|
||||
|
||||
const EditForm = ({
|
||||
editForm,
|
||||
divRef,
|
||||
isSticky
|
||||
}: {
|
||||
editForm: UseFormReturn<AppSimpleEditFormType, any>;
|
||||
divRef: React.RefObject<HTMLDivElement>;
|
||||
isSticky: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { appDetail, publishApp } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const { allDatasets } = useDatasetStore();
|
||||
const { llmModelList } = useSystemStore();
|
||||
const [, startTst] = useTransition();
|
||||
const refresh = useUpdate();
|
||||
|
||||
const { setValue, getValues, handleSubmit, control, watch } = editForm;
|
||||
|
||||
const { fields: datasets, replace: replaceDatasetList } = useFieldArray({
|
||||
control,
|
||||
name: 'dataset.datasets'
|
||||
});
|
||||
const selectDatasets = useMemo(
|
||||
() => allDatasets.filter((item) => datasets.find((dataset) => dataset.datasetId === item._id)),
|
||||
[allDatasets, datasets]
|
||||
);
|
||||
// useEffect(() => {
|
||||
// if (selectDatasets.length !== datasets.length) {
|
||||
// replaceDatasetList(
|
||||
// selectDatasets.map((item) => ({
|
||||
// datasetId: item._id
|
||||
// }))
|
||||
// );
|
||||
// }
|
||||
// }, [datasets, replaceDatasetList, selectDatasets]);
|
||||
|
||||
const {
|
||||
isOpen: isOpenDatasetSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenDatasetParams,
|
||||
onOpen: onOpenDatasetParams,
|
||||
onClose: onCloseDatasetParams
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenToolsSelect,
|
||||
onOpen: onOpenToolsSelect,
|
||||
onClose: onCloseToolsSelect
|
||||
} = useDisclosure();
|
||||
|
||||
const { openConfirm: openConfirmSave, ConfirmModal: ConfirmSaveModal } = useConfirm({
|
||||
content: t('core.app.edit.Confirm Save App Tip')
|
||||
});
|
||||
|
||||
const aiSystemPrompt = watch('aiSettings.systemPrompt');
|
||||
const selectLLMModel = watch('aiSettings.model');
|
||||
const datasetSearchSetting = watch('dataset');
|
||||
const variables = watch('chatConfig.variables');
|
||||
|
||||
const formatVariables: any = useMemo(
|
||||
() => formatEditorVariablePickerIcon([...getSystemVariables(t), ...(variables || [])]),
|
||||
[t, variables]
|
||||
);
|
||||
const tts = getValues('chatConfig.ttsConfig');
|
||||
const whisperConfig = getValues('chatConfig.whisperConfig');
|
||||
const postQuestionGuide = getValues('chatConfig.questionGuide');
|
||||
const selectedTools = watch('selectedTools');
|
||||
const inputGuideConfig = watch('chatConfig.chatInputGuide');
|
||||
const scheduledTriggerConfig = watch('chatConfig.scheduledTriggerConfig');
|
||||
const searchMode = watch('dataset.searchMode');
|
||||
|
||||
const tokenLimit = useMemo(() => {
|
||||
return llmModelList.find((item) => item.model === selectLLMModel)?.quoteMaxToken || 3000;
|
||||
}, [selectLLMModel, llmModelList]);
|
||||
|
||||
/* on save app */
|
||||
const { mutate: onSubmitPublish, isLoading: isSaving } = useRequest({
|
||||
mutationFn: async (data: AppSimpleEditFormType) => {
|
||||
const { nodes, edges } = form2AppWorkflow(data);
|
||||
await publishApp({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: data.chatConfig,
|
||||
type: AppTypeEnum.simple
|
||||
});
|
||||
},
|
||||
successToast: t('common.Save Success'),
|
||||
errorToast: t('common.Save Failed')
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const wat = watch((data) => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
return () => {
|
||||
wat.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* title */}
|
||||
<Flex
|
||||
ref={divRef}
|
||||
position={'sticky'}
|
||||
top={-4}
|
||||
bg={'myGray.25'}
|
||||
py={4}
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
zIndex={100}
|
||||
px={4}
|
||||
{...(isSticky && {
|
||||
borderBottom: theme.borders.base,
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.12)'
|
||||
})}
|
||||
>
|
||||
<HStack>
|
||||
<Box color={'myGray.900'}>{t('core.app.App params config')}</Box>
|
||||
<QuestionTip label={t('core.app.Simple Config Tip')} />
|
||||
</HStack>
|
||||
<Button
|
||||
isLoading={isSaving}
|
||||
size={['sm', 'md']}
|
||||
leftIcon={
|
||||
appDetail.type === AppTypeEnum.simple ? (
|
||||
<MyIcon name={'common/publishFill'} w={['14px', '16px']} />
|
||||
) : undefined
|
||||
}
|
||||
variant={appDetail.type === AppTypeEnum.simple ? 'primary' : 'whitePrimary'}
|
||||
onClick={() => {
|
||||
if (appDetail.type !== AppTypeEnum.simple) {
|
||||
openConfirmSave(handleSubmit((data) => onSubmitPublish(data)))();
|
||||
} else {
|
||||
handleSubmit((data) => onSubmitPublish(data))();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{appDetail.type !== AppTypeEnum.simple
|
||||
? t('core.app.Change to simple mode')
|
||||
: t('core.app.Publish')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Box px={4}>
|
||||
<Box bg={'white'} borderRadius={'md'} borderWidth={'1px'} borderColor={'borderColor.base'}>
|
||||
{/* ai */}
|
||||
<Box {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
|
||||
<FormLabel ml={2} flex={1}>
|
||||
{appT('AI Settings')}
|
||||
</FormLabel>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box {...LabelStyles}>{t('core.ai.Model')}</Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<SettingLLMModel
|
||||
llmModelType={'all'}
|
||||
defaultData={{
|
||||
model: getValues('aiSettings.model'),
|
||||
temperature: getValues('aiSettings.temperature'),
|
||||
maxToken: getValues('aiSettings.maxToken'),
|
||||
maxHistories: getValues('aiSettings.maxHistories')
|
||||
}}
|
||||
onChange={({ model, temperature, maxToken, maxHistories }: SettingAIDataType) => {
|
||||
setValue('aiSettings.model', model);
|
||||
setValue('aiSettings.maxToken', maxToken);
|
||||
setValue('aiSettings.temperature', temperature);
|
||||
setValue('aiSettings.maxHistories', maxHistories ?? 6);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Box mt={3}>
|
||||
<HStack {...LabelStyles}>
|
||||
<Box>{t('core.ai.Prompt')}</Box>
|
||||
<QuestionTip label={t('core.app.tip.chatNodeSystemPromptTip')} />
|
||||
</HStack>
|
||||
<Box mt={1}>
|
||||
<PromptEditor
|
||||
value={aiSystemPrompt}
|
||||
onChange={(text) => {
|
||||
startTst(() => {
|
||||
setValue('aiSettings.systemPrompt', text);
|
||||
});
|
||||
}}
|
||||
variables={formatVariables}
|
||||
placeholder={t('core.app.tip.chatNodeSystemPromptTip')}
|
||||
title={t('core.ai.Prompt')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* dataset */}
|
||||
<Box {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'} flex={1}>
|
||||
<MyIcon name={'core/app/simpleMode/dataset'} w={'20px'} />
|
||||
<FormLabel ml={2}>{t('core.dataset.Choose Dataset')}</FormLabel>
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'transparentBase'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
fontSize={'sm'}
|
||||
onClick={onOpenKbSelect}
|
||||
>
|
||||
{t('common.Choose')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'transparentBase'}
|
||||
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
fontSize={'sm'}
|
||||
onClick={onOpenDatasetParams}
|
||||
>
|
||||
{t('common.Params')}
|
||||
</Button>
|
||||
</Flex>
|
||||
{datasetSearchSetting.datasets?.length > 0 && (
|
||||
<Box my={3}>
|
||||
<SearchParamsTip
|
||||
searchMode={searchMode}
|
||||
similarity={getValues('dataset.similarity')}
|
||||
limit={getValues('dataset.limit')}
|
||||
usingReRank={getValues('dataset.usingReRank')}
|
||||
queryExtensionModel={getValues('dataset.datasetSearchExtensionModel')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Grid
|
||||
gridTemplateColumns={['repeat(2, minmax(0, 1fr))', 'repeat(3, minmax(0, 1fr))']}
|
||||
gridGap={[2, 4]}
|
||||
>
|
||||
{selectDatasets.map((item) => (
|
||||
<MyTooltip key={item._id} label={t('core.dataset.Read Dataset')}>
|
||||
<Flex
|
||||
overflow={'hidden'}
|
||||
alignItems={'center'}
|
||||
p={2}
|
||||
bg={'white'}
|
||||
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
|
||||
borderRadius={'md'}
|
||||
border={theme.borders.base}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
router.push({
|
||||
pathname: '/dataset/detail',
|
||||
query: {
|
||||
datasetId: item._id
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'18px'} mr={1} />
|
||||
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* tool choice */}
|
||||
<Box {...BoxStyles}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'} flex={1}>
|
||||
<MyIcon name={'core/app/toolCall'} w={'20px'} />
|
||||
<FormLabel ml={2}>{t('core.app.Tool call')}(实验功能)</FormLabel>
|
||||
<QuestionTip ml={1} label={t('core.app.Tool call tip')} />
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'transparentBase'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
mr={'-5px'}
|
||||
size={'sm'}
|
||||
fontSize={'sm'}
|
||||
onClick={onOpenToolsSelect}
|
||||
>
|
||||
{t('common.Choose')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Grid
|
||||
mt={selectedTools.length > 0 ? 2 : 0}
|
||||
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
|
||||
gridGap={[2, 4]}
|
||||
>
|
||||
{selectedTools.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
overflow={'hidden'}
|
||||
alignItems={'center'}
|
||||
p={2}
|
||||
bg={'white'}
|
||||
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
|
||||
borderRadius={'md'}
|
||||
border={theme.borders.base}
|
||||
_hover={{
|
||||
...hoverDeleteStyles,
|
||||
borderColor: 'primary.300'
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'18px'} mr={1} />
|
||||
<Box flex={'1 0 0'} w={0} className={'textEllipsis'} fontSize={'sm'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<DeleteIcon
|
||||
onClick={() => {
|
||||
setValue(
|
||||
'selectedTools',
|
||||
selectedTools.filter((tool) => tool.id !== item.id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* variable */}
|
||||
<Box {...BoxStyles}>
|
||||
<VariableEdit
|
||||
variables={variables}
|
||||
onChange={(e) => {
|
||||
setValue('chatConfig.variables', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* welcome */}
|
||||
<Box {...BoxStyles}>
|
||||
<WelcomeTextConfig
|
||||
defaultValue={getValues('chatConfig.welcomeText')}
|
||||
onBlur={(e) => {
|
||||
setValue('chatConfig.welcomeText', e.target.value || '');
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* tts */}
|
||||
<Box {...BoxStyles}>
|
||||
<TTSSelect
|
||||
value={tts}
|
||||
onChange={(e) => {
|
||||
setValue('chatConfig.ttsConfig', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* whisper */}
|
||||
<Box {...BoxStyles}>
|
||||
<WhisperConfig
|
||||
isOpenAudio={tts?.type !== TTSTypeEnum.none}
|
||||
value={whisperConfig}
|
||||
onChange={(e) => {
|
||||
setValue('chatConfig.whisperConfig', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* question guide */}
|
||||
<Box {...BoxStyles}>
|
||||
<QGSwitch
|
||||
isChecked={postQuestionGuide}
|
||||
onChange={(e) => {
|
||||
setValue('chatConfig.questionGuide', e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* question tips */}
|
||||
<Box {...BoxStyles}>
|
||||
<InputGuideConfig
|
||||
appId={appDetail._id}
|
||||
value={inputGuideConfig}
|
||||
onChange={(e) => {
|
||||
setValue('chatConfig.chatInputGuide', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* timer trigger */}
|
||||
<Box {...BoxStyles} borderBottom={'none'}>
|
||||
<ScheduledTriggerConfig
|
||||
value={scheduledTriggerConfig}
|
||||
onChange={(e) => {
|
||||
setValue('chatConfig.scheduledTriggerConfig', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ConfirmSaveModal bg={appDetail.type === AppTypeEnum.simple ? '' : 'red.600'} countDown={5} />
|
||||
{isOpenDatasetSelect && (
|
||||
<DatasetSelectModal
|
||||
isOpen={isOpenDatasetSelect}
|
||||
defaultSelectedDatasets={selectDatasets.map((item) => ({
|
||||
datasetId: item._id,
|
||||
vectorModel: item.vectorModel
|
||||
}))}
|
||||
onClose={onCloseKbSelect}
|
||||
onChange={replaceDatasetList}
|
||||
/>
|
||||
)}
|
||||
{isOpenDatasetParams && (
|
||||
<DatasetParamsModal
|
||||
{...datasetSearchSetting}
|
||||
maxTokens={tokenLimit}
|
||||
onClose={onCloseDatasetParams}
|
||||
onSuccess={(e) => {
|
||||
setValue('dataset', {
|
||||
...getValues('dataset'),
|
||||
...e
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isOpenToolsSelect && (
|
||||
<ToolSelectModal
|
||||
selectedTools={selectedTools}
|
||||
onAddTool={(e) => {
|
||||
setValue('selectedTools', [...selectedTools, e]);
|
||||
}}
|
||||
onRemoveTool={(e) => {
|
||||
setValue(
|
||||
'selectedTools',
|
||||
selectedTools.filter((item) => item.pluginId !== e.pluginId)
|
||||
);
|
||||
}}
|
||||
onClose={onCloseToolsSelect}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EditForm);
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Grid } from '@chakra-ui/react';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useSticky } from '@/web/common/hooks/useSticky';
|
||||
import { useMount } from 'ahooks';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { appWorkflow2Form, getDefaultAppForm } from '@fastgpt/global/core/app/utils';
|
||||
|
||||
import ChatTest from './ChatTest';
|
||||
import AppCard from './AppCard';
|
||||
import EditForm from './EditForm';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
|
||||
const SimpleEdit = ({ appId }: { appId: string }) => {
|
||||
const { isPc } = useSystemStore();
|
||||
const { parentRef, divRef, isSticky } = useSticky();
|
||||
const { loadAllDatasets } = useDatasetStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const editForm = useForm<AppSimpleEditFormType>({
|
||||
defaultValues: getDefaultAppForm()
|
||||
});
|
||||
|
||||
// show selected dataset
|
||||
useMount(() => {
|
||||
loadAllDatasets();
|
||||
editForm.reset(
|
||||
appWorkflow2Form({
|
||||
nodes: appDetail.modules,
|
||||
chatConfig: appDetail.chatConfig
|
||||
})
|
||||
);
|
||||
|
||||
if (appDetail.version !== 'v2') {
|
||||
editForm.reset(
|
||||
appWorkflow2Form({
|
||||
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
|
||||
chatConfig: appDetail.chatConfig
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid gridTemplateColumns={['1fr', '560px 1fr']} h={'100%'}>
|
||||
<Box
|
||||
ref={parentRef}
|
||||
h={'100%'}
|
||||
borderRight={'1.5px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
pt={[0, 4]}
|
||||
pb={10}
|
||||
overflow={'overlay'}
|
||||
>
|
||||
<AppCard />
|
||||
|
||||
<Box mt={2}>
|
||||
<EditForm editForm={editForm} divRef={divRef} isSticky={isSticky} />
|
||||
</Box>
|
||||
</Box>
|
||||
{isPc && <ChatTest editForm={editForm} appId={appId} />}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SimpleEdit);
|
||||
@@ -23,7 +23,7 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getTeamsTags } from '@/web/support/user/team/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
|
||||
const TagsEditModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
198
projects/app/src/pages/app/detail/components/Workflow/Header.tsx
Normal file
198
projects/app/src/pages/app/detail/components/Workflow/Header.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext, getWorkflowStore } from '../WorkflowComponents/context';
|
||||
import { useInterval } from 'ahooks';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import RouteTab from '../RouteTab';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import AppCard from '../WorkflowComponents/AppCard';
|
||||
import { uiWorkflow2StoreWorkflow } from '../WorkflowComponents/utils';
|
||||
const PublishHistories = dynamic(() => import('../PublishHistoriesSlider'));
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystemStore();
|
||||
const router = useRouter();
|
||||
|
||||
const { appDetail, onPublish, currentTab } = useContextSelector(AppContext, (v) => v);
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
|
||||
const {
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
onSaveWorkflow,
|
||||
setHistoriesDefaultData,
|
||||
historiesDefaultData,
|
||||
initData
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const onclickPublish = useCallback(async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
await onPublish({
|
||||
...data,
|
||||
chatConfig: appDetail.chatConfig,
|
||||
//@ts-ignore
|
||||
version: 'v2'
|
||||
});
|
||||
}
|
||||
}, [flowData2StoreDataAndCheck, onPublish, appDetail.chatConfig]);
|
||||
|
||||
const saveAndBack = useCallback(async () => {
|
||||
try {
|
||||
await onSaveWorkflow();
|
||||
router.push('/app/list');
|
||||
} catch (error) {}
|
||||
}, [onSaveWorkflow, router]);
|
||||
// effect
|
||||
useBeforeunload({
|
||||
callback: onSaveWorkflow,
|
||||
tip: t('core.common.tip.leave page')
|
||||
});
|
||||
useInterval(() => {
|
||||
if (!appDetail._id) return;
|
||||
onSaveWorkflow();
|
||||
}, 40000);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
{!isPc && (
|
||||
<Flex pt={2} justifyContent={'center'}>
|
||||
<RouteTab />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
mt={[2, 0]}
|
||||
py={3}
|
||||
pl={[2, 4]}
|
||||
pr={[2, 6]}
|
||||
borderBottom={'base'}
|
||||
alignItems={'center'}
|
||||
userSelect={'none'}
|
||||
h={'67px'}
|
||||
{...(currentTab === TabEnum.appEdit
|
||||
? {
|
||||
bg: 'myGray.25'
|
||||
}
|
||||
: {
|
||||
bg: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
})}
|
||||
>
|
||||
{/* back */}
|
||||
<MyIcon
|
||||
name={'common/leftArrowLight'}
|
||||
w={'1.75rem'}
|
||||
cursor={'pointer'}
|
||||
onClick={saveAndBack}
|
||||
/>
|
||||
{/* app info */}
|
||||
<Box ml={1}>
|
||||
<AppCard
|
||||
showSaveStatus={
|
||||
!historiesDefaultData && isV2Workflow && currentTab === TabEnum.appEdit
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{isPc && (
|
||||
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
|
||||
<RouteTab />
|
||||
</Box>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
|
||||
{currentTab === TabEnum.appEdit && (
|
||||
<>
|
||||
{!historiesDefaultData && (
|
||||
<IconButton
|
||||
mr={[2, 4]}
|
||||
icon={<MyIcon name={'history'} w={'18px'} />}
|
||||
aria-label={''}
|
||||
size={'sm'}
|
||||
w={'30px'}
|
||||
variant={'whitePrimary'}
|
||||
onClick={async () => {
|
||||
const { nodes, edges } = uiWorkflow2StoreWorkflow(await getWorkflowStore());
|
||||
|
||||
setHistoriesDefaultData({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
|
||||
variant={'whitePrimary'}
|
||||
onClick={async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
setWorkflowTestData(data);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('core.workflow.Debug')}
|
||||
</Button>
|
||||
|
||||
{!historiesDefaultData && (
|
||||
<PopoverConfirm
|
||||
showCancel
|
||||
content={t('core.app.Publish Confirm')}
|
||||
Trigger={
|
||||
<Button
|
||||
ml={[2, 4]}
|
||||
size={'sm'}
|
||||
leftIcon={<MyIcon name={'common/publishFill'} w={['14px', '16px']} />}
|
||||
>
|
||||
{t('core.app.Publish')}
|
||||
</Button>
|
||||
}
|
||||
onConfirm={() => onclickPublish()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
{historiesDefaultData && (
|
||||
<PublishHistories
|
||||
initData={initData}
|
||||
onClose={() => {
|
||||
setHistoriesDefaultData(undefined);
|
||||
}}
|
||||
defaultData={historiesDefaultData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
isPc,
|
||||
currentTab,
|
||||
saveAndBack,
|
||||
historiesDefaultData,
|
||||
isV2Workflow,
|
||||
t,
|
||||
initData,
|
||||
setHistoriesDefaultData,
|
||||
appDetail.chatConfig,
|
||||
flowData2StoreDataAndCheck,
|
||||
setWorkflowTestData,
|
||||
onclickPublish
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(Header);
|
||||
@@ -1,21 +1,25 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type.d';
|
||||
import Header from './Header';
|
||||
import Flow from '@/components/core/workflow/Flow';
|
||||
import React from 'react';
|
||||
import { appSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
|
||||
import WorkflowContextProvider, { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import WorkflowContextProvider, { WorkflowContext } from '../WorkflowComponents/context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import { useMount } from 'ahooks';
|
||||
import Header from './Header';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { workflowBoxStyles } from '../constants';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
type Props = { onClose: () => void };
|
||||
|
||||
const Render = ({ onClose }: Props) => {
|
||||
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
|
||||
import Flow from '../WorkflowComponents/Flow';
|
||||
const Logs = dynamic(() => import('../Logs/index'));
|
||||
const PublishChannel = dynamic(() => import('../Publish'));
|
||||
|
||||
const WorkflowEdit = () => {
|
||||
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
|
||||
const isV2Workflow = appDetail?.version === 'v2';
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
showCancel: false,
|
||||
content:
|
||||
@@ -24,47 +28,45 @@ const Render = ({ onClose }: Props) => {
|
||||
|
||||
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
|
||||
|
||||
const workflowStringData = JSON.stringify({
|
||||
nodes: appDetail.modules || [],
|
||||
edges: appDetail.edges || []
|
||||
});
|
||||
|
||||
useMount(() => {
|
||||
if (!isV2Workflow) {
|
||||
openConfirm(() => {
|
||||
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
|
||||
})();
|
||||
} else {
|
||||
initData(JSON.parse(workflowStringData));
|
||||
initData(
|
||||
cloneDeep({
|
||||
nodes: appDetail.modules || [],
|
||||
edges: appDetail.edges || []
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const memoRender = useMemo(() => {
|
||||
return <Flow Header={<Header onClose={onClose} />} />;
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{memoRender}
|
||||
<Flex {...workflowBoxStyles}>
|
||||
<Header />
|
||||
|
||||
{currentTab === TabEnum.appEdit ? (
|
||||
<Flow />
|
||||
) : (
|
||||
<Flex flexDirection={'column'} h={'100%'} px={4} pb={4}>
|
||||
{currentTab === TabEnum.publish && <PublishChannel />}
|
||||
{currentTab === TabEnum.logs && <Logs />}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!isV2Workflow && <ConfirmModal countDown={0} />}
|
||||
</>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(function FlowEdit(props: Props) {
|
||||
const appDetail = useContextSelector(AppContext, (e) => e.appDetail);
|
||||
const filterAppIds = useMemo(() => [appDetail._id], [appDetail._id]);
|
||||
|
||||
const Render = () => {
|
||||
return (
|
||||
<WorkflowContextProvider
|
||||
value={{
|
||||
appId: appDetail._id,
|
||||
mode: 'app',
|
||||
filterAppIds,
|
||||
basicNodeTemplates: appSystemModuleTemplates
|
||||
}}
|
||||
>
|
||||
<Render {...props} />
|
||||
<WorkflowContextProvider basicNodeTemplates={appSystemModuleTemplates}>
|
||||
<WorkflowEdit />
|
||||
</WorkflowContextProvider>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
@@ -0,0 +1,222 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, HStack, useDisclosure } from '@chakra-ui/react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext, TabEnum } from '../context';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { WorkflowContext } from './context';
|
||||
import { compareWorkflow, filterSensitiveNodesData } from '@/web/core/workflow/utils';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { publishStatusStyle } from '../constants';
|
||||
|
||||
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
|
||||
|
||||
const AppCard = ({ showSaveStatus }: { showSaveStatus: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
const { copyData } = useCopyData();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { appDetail, appLatestVersion, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
|
||||
useContextSelector(AppContext, (v) => v);
|
||||
const { historiesDefaultData, flowData2StoreDataAndCheck, onSaveWorkflow, isSaving, saveLabel } =
|
||||
useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
const onExportWorkflow = useCallback(async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
copyData(
|
||||
JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
appT('Export Config Successful')
|
||||
);
|
||||
}
|
||||
}, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
|
||||
|
||||
const isPublished = (() => {
|
||||
const data = flowData2StoreDataAndCheck(true);
|
||||
if (!appLatestVersion) return true;
|
||||
|
||||
if (data) {
|
||||
return compareWorkflow(
|
||||
{
|
||||
nodes: appLatestVersion.nodes,
|
||||
edges: appLatestVersion.edges,
|
||||
chatConfig: appLatestVersion.chatConfig
|
||||
},
|
||||
{
|
||||
nodes: data.nodes,
|
||||
edges: data.edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
}
|
||||
);
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
const InfoMenu = useCallback(
|
||||
({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<MyMenu
|
||||
width={150}
|
||||
Button={children}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'edit',
|
||||
label: appT('Edit info'),
|
||||
onClick: onOpenInfoEdit
|
||||
},
|
||||
{
|
||||
icon: 'support/team/key',
|
||||
label: t('common.Role'),
|
||||
onClick: onOpenInfoEdit
|
||||
}
|
||||
]
|
||||
},
|
||||
...(!historiesDefaultData && currentTab === TabEnum.appEdit
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: appT('Import Configs'),
|
||||
icon: 'common/importLight',
|
||||
onClick: onOpenImport
|
||||
},
|
||||
{
|
||||
label: appT('Export Configs'),
|
||||
icon: 'export',
|
||||
onClick: onExportWorkflow
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'support/team/memberLight',
|
||||
label: t('common.Team Tags Set'),
|
||||
onClick: onOpenTeamTagModal
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(appDetail.permission.isOwner
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
type: 'danger' as 'danger',
|
||||
icon: 'delete',
|
||||
label: t('common.Delete'),
|
||||
onClick: onDelApp
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
appDetail.permission.hasWritePer,
|
||||
appDetail.permission.isOwner,
|
||||
appT,
|
||||
currentTab,
|
||||
feConfigs?.show_team_chat,
|
||||
historiesDefaultData,
|
||||
onDelApp,
|
||||
onExportWorkflow,
|
||||
onOpenImport,
|
||||
onOpenInfoEdit,
|
||||
onOpenTeamTagModal,
|
||||
t
|
||||
]
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<HStack>
|
||||
<InfoMenu>
|
||||
<Avatar src={appDetail.avatar} w={'1.75rem'} />
|
||||
</InfoMenu>
|
||||
<Box>
|
||||
<InfoMenu>
|
||||
<HStack spacing={1} cursor={'pointer'}>
|
||||
<Box color={'myGray.900'}>{appDetail.name}</Box>
|
||||
<MyIcon name={'common/select'} w={'1rem'} />
|
||||
</HStack>
|
||||
</InfoMenu>
|
||||
{showSaveStatus && (
|
||||
<MyTooltip label={t('core.app.Onclick to save')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
h={'20px'}
|
||||
cursor={'pointer'}
|
||||
fontSize={'mini'}
|
||||
onClick={onSaveWorkflow}
|
||||
lineHeight={1}
|
||||
>
|
||||
{isSaving && <MyIcon name={'common/loading'} w={'0.8rem'} mr={0.5} />}
|
||||
<Box color={'myGray.500'}>{saveLabel}</Box>
|
||||
<MyTag
|
||||
py={0}
|
||||
showDot
|
||||
bg={'transparent'}
|
||||
colorSchema={
|
||||
isPublished
|
||||
? publishStatusStyle.published.colorSchema
|
||||
: publishStatusStyle.unPublish.colorSchema
|
||||
}
|
||||
>
|
||||
{isPublished
|
||||
? publishStatusStyle.published.text
|
||||
: publishStatusStyle.unPublish.text}
|
||||
</MyTag>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
|
||||
</HStack>
|
||||
);
|
||||
}, [
|
||||
InfoMenu,
|
||||
appDetail.avatar,
|
||||
appDetail.name,
|
||||
isOpenImport,
|
||||
isPublished,
|
||||
isSaving,
|
||||
onCloseImport,
|
||||
onSaveWorkflow,
|
||||
saveLabel,
|
||||
showSaveStatus,
|
||||
t
|
||||
]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default AppCard;
|
||||
@@ -1,36 +1,17 @@
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type.d';
|
||||
import React, {
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
ForwardedRef
|
||||
} from 'react';
|
||||
import React, { useRef, forwardRef, ForwardedRef, useImperativeHandle } from 'react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { streamFetch } from '@/web/common/api/fetch';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import ChatBox from '@/components/ChatBox';
|
||||
import type { ComponentRef, StartChatFnProps } from '@/components/ChatBox/type.d';
|
||||
import {
|
||||
checkChatSupportSelectFileByModules,
|
||||
getAppQuestionGuidesByModules
|
||||
} from '@/web/core/chat/utils';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import type { ComponentRef } from '@/components/ChatBox/type.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
import {
|
||||
getDefaultEntryNodeIds,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
initWorkflowEdgeStatus,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import { useChatTest } from '@/pages/app/detail/components/useChatTest';
|
||||
|
||||
export type ChatTestComponentRef = {
|
||||
resetChatTest: () => void;
|
||||
@@ -52,48 +33,32 @@ const ChatTest = (
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const ChatBoxRef = useRef<ComponentRef>(null);
|
||||
const { userInfo } = useUserStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const startChat = useCallback(
|
||||
async ({ chatList, controller, generatingMessage, variables }: StartChatFnProps) => {
|
||||
/* get histories */
|
||||
let historyMaxLen = getMaxHistoryLimitFromNodes(nodes);
|
||||
const history = chatList.slice(-historyMaxLen - 2, -2);
|
||||
|
||||
// 流请求,获取数据
|
||||
const { responseText, responseData } = await streamFetch({
|
||||
url: '/api/core/chat/chatTest',
|
||||
data: {
|
||||
history,
|
||||
prompt: chatList[chatList.length - 2].value,
|
||||
nodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
|
||||
edges: initWorkflowEdgeStatus(edges),
|
||||
variables,
|
||||
appId: appDetail._id,
|
||||
appName: `调试-${appDetail.name}`,
|
||||
mode: 'test'
|
||||
},
|
||||
onMessage: generatingMessage,
|
||||
abortCtrl: controller
|
||||
});
|
||||
|
||||
return { responseText, responseData };
|
||||
},
|
||||
[appDetail._id, appDetail.name, edges, nodes]
|
||||
);
|
||||
const { resetChatBox, ChatBox } = useChatTest({
|
||||
nodes,
|
||||
edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetChatTest() {
|
||||
ChatBoxRef.current?.resetHistory([]);
|
||||
ChatBoxRef.current?.resetVariables();
|
||||
}
|
||||
resetChatTest: resetChatBox
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={300}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'fixed'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Flex
|
||||
zIndex={101}
|
||||
zIndex={300}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
@@ -127,7 +92,7 @@ const ChatTest = (
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('common.Close')}>
|
||||
<IconButton
|
||||
ml={[3, 6]}
|
||||
ml={3}
|
||||
icon={<SmallCloseIcon fontSize={'22px'} />}
|
||||
variant={'grayBase'}
|
||||
size={'smSquare'}
|
||||
@@ -137,29 +102,9 @@ const ChatTest = (
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
appId={appDetail._id}
|
||||
appAvatar={appDetail.avatar}
|
||||
userAvatar={userInfo?.avatar}
|
||||
showMarkIcon
|
||||
chatConfig={appDetail.chatConfig}
|
||||
showFileSelector={checkChatSupportSelectFileByModules(nodes)}
|
||||
onStartChat={startChat}
|
||||
onDelMessage={() => {}}
|
||||
/>
|
||||
<ChatBox />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'fixed'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,24 +11,24 @@ import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { getPreviewPluginModule } from '@/web/core/plugin/api';
|
||||
import { getPreviewPluginNode, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { workflowNodeTemplateList } from '@fastgpt/web/core/workflow/constants';
|
||||
import RowTabs from '@fastgpt/web/components/common/Tabs/RowTabs';
|
||||
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import ParentPaths from '@/components/common/ParentPaths';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { debounce } from 'lodash';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useCreation } from 'ahooks';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
|
||||
type ModuleTemplateListProps = {
|
||||
isOpen: boolean;
|
||||
@@ -37,8 +37,8 @@ type ModuleTemplateListProps = {
|
||||
type RenderListProps = {
|
||||
templates: FlowNodeTemplateType[];
|
||||
onClose: () => void;
|
||||
currentParent?: { parentId: string; parentName: string };
|
||||
setCurrentParent: (e: { parentId: string; parentName: string }) => void;
|
||||
parentId: ParentIdType;
|
||||
setParentId: React.Dispatch<React.SetStateAction<ParentIdType>>;
|
||||
};
|
||||
|
||||
enum TemplateTypeEnum {
|
||||
@@ -52,195 +52,175 @@ const sliderWidth = 390;
|
||||
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [currentParent, setCurrentParent] = useState<RenderListProps['currentParent']>();
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const { feConfigs } = useSystemStore();
|
||||
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
|
||||
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const { basicNodeTemplates, hasToolNode, nodeList } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const {
|
||||
systemNodeTemplates,
|
||||
loadSystemNodeTemplates,
|
||||
teamPluginNodeTemplates,
|
||||
loadTeamPluginNodeTemplates
|
||||
} = useWorkflowStore();
|
||||
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
|
||||
|
||||
const templates = useCreation(() => {
|
||||
const map = {
|
||||
[TemplateTypeEnum.basic]: basicNodeTemplates.filter((item) => {
|
||||
// unique node filter
|
||||
if (item.unique) {
|
||||
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
|
||||
if (nodeExist) {
|
||||
const { data: templates = [], loading } = useRequest2(
|
||||
async () => {
|
||||
if (templateType === TemplateTypeEnum.basic) {
|
||||
return basicNodeTemplates.filter((item) => {
|
||||
// unique node filter
|
||||
if (item.unique) {
|
||||
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
|
||||
if (nodeExist) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// special node filter
|
||||
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// special node filter
|
||||
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
|
||||
return false;
|
||||
}
|
||||
// tool stop
|
||||
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
[TemplateTypeEnum.systemPlugin]: systemNodeTemplates,
|
||||
[TemplateTypeEnum.teamPlugin]: teamPluginNodeTemplates.filter((item) =>
|
||||
searchKey ? item.pluginType !== PluginTypeEnum.folder : true
|
||||
)
|
||||
};
|
||||
return map[templateType];
|
||||
}, [
|
||||
basicNodeTemplates,
|
||||
feConfigs.lafEnv,
|
||||
hasToolNode,
|
||||
nodeList,
|
||||
searchKey,
|
||||
systemNodeTemplates,
|
||||
teamPluginNodeTemplates,
|
||||
templateType
|
||||
]);
|
||||
|
||||
const { mutate: onChangeTab } = useRequest({
|
||||
mutationFn: async (e: any) => {
|
||||
const val = e as TemplateTypeEnum;
|
||||
if (val === TemplateTypeEnum.systemPlugin) {
|
||||
await loadSystemNodeTemplates();
|
||||
} else if (val === TemplateTypeEnum.teamPlugin) {
|
||||
await loadTeamPluginNodeTemplates({
|
||||
parentId: currentParent?.parentId
|
||||
// tool stop
|
||||
if (!hasToolNode && item.flowNodeType === FlowNodeTypeEnum.stopTool) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
setTemplateType(val);
|
||||
if (templateType === TemplateTypeEnum.systemPlugin) {
|
||||
return getSystemPlugTemplates();
|
||||
}
|
||||
if (templateType === TemplateTypeEnum.teamPlugin) {
|
||||
return getTeamPlugTemplates({
|
||||
parentId,
|
||||
searchKey,
|
||||
type: [AppTypeEnum.folder, AppTypeEnum.httpPlugin, AppTypeEnum.plugin]
|
||||
});
|
||||
}
|
||||
return [];
|
||||
},
|
||||
errorToast: t('core.module.templates.Load plugin error')
|
||||
{
|
||||
manual: false,
|
||||
throttleWait: 300,
|
||||
refreshDeps: [basicNodeTemplates, nodeList, hasToolNode, templateType, searchKey, parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const { data: paths = [] } = useRequest2(() => getAppFolderPath(parentId), {
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
});
|
||||
|
||||
useQuery(['teamNodeTemplate', currentParent?.parentId, searchKey], () =>
|
||||
loadTeamPluginNodeTemplates({
|
||||
parentId: currentParent?.parentId,
|
||||
searchKey,
|
||||
init: true
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
w={`${sliderWidth}px`}
|
||||
onClick={onClose}
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
<Flex
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'10px'}
|
||||
left={0}
|
||||
pt={'20px'}
|
||||
pb={4}
|
||||
h={isOpen ? 'calc(100% - 20px)' : '0'}
|
||||
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'0 20px 20px 0'}
|
||||
transition={'.2s ease'}
|
||||
userSelect={'none'}
|
||||
overflow={isOpen ? 'none' : 'hidden'}
|
||||
>
|
||||
<Box mb={2} pl={'20px'} pr={'10px'} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
|
||||
<RowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'core/modules/basicNode',
|
||||
label: t('core.module.template.Basic Node'),
|
||||
value: TemplateTypeEnum.basic
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/systemPlugin',
|
||||
label: t('core.module.template.System Plugin'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('core.module.template.Team Plugin'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
py={'5px'}
|
||||
value={templateType}
|
||||
onChange={onChangeTab}
|
||||
/>
|
||||
{/* close icon */}
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'grayBase'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Flex>
|
||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
||||
<Flex mt={2} alignItems={'center'} h={10}>
|
||||
<InputGroup mr={4} h={'full'}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
h={'full'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('plugin.Search plugin')}
|
||||
onChange={debounce((e) => setSearchKey(e.target.value), 200)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Box flex={1} />
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => router.push('/plugin/list')}
|
||||
>
|
||||
<Box>去创建</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'14px'} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && currentParent && (
|
||||
<Flex alignItems={'center'} mt={2}>
|
||||
<ParentPaths
|
||||
paths={[currentParent]}
|
||||
FirstPathDom={null}
|
||||
onClick={() => {
|
||||
setCurrentParent(undefined);
|
||||
}}
|
||||
fontSize="md"
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
w={`${sliderWidth}px`}
|
||||
onClick={onClose}
|
||||
fontSize={'sm'}
|
||||
/>
|
||||
<MyBox
|
||||
isLoading={loading}
|
||||
display={'flex'}
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'10px'}
|
||||
left={0}
|
||||
pt={'20px'}
|
||||
pb={4}
|
||||
h={isOpen ? 'calc(100% - 20px)' : '0'}
|
||||
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'0 20px 20px 0'}
|
||||
transition={'.2s ease'}
|
||||
userSelect={'none'}
|
||||
overflow={isOpen ? 'none' : 'hidden'}
|
||||
>
|
||||
<Box pl={'20px'} mb={3} pr={'10px'} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
|
||||
<RowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'core/modules/basicNode',
|
||||
label: t('core.module.template.Basic Node'),
|
||||
value: TemplateTypeEnum.basic
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/systemPlugin',
|
||||
label: t('core.module.template.System Plugin'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('core.module.template.Team Plugin'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
py={'5px'}
|
||||
value={templateType}
|
||||
onChange={(e) => setTemplateType(e as TemplateTypeEnum)}
|
||||
/>
|
||||
{/* close icon */}
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'grayBase'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
<RenderList
|
||||
templates={templates}
|
||||
onClose={onClose}
|
||||
currentParent={currentParent}
|
||||
setCurrentParent={setCurrentParent}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
||||
<Flex mt={2} alignItems={'center'} h={10}>
|
||||
<InputGroup mr={4} h={'full'}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
h={'full'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('plugin.Search plugin')}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Box flex={1} />
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => router.push('/app/list')}
|
||||
>
|
||||
<Box>去创建</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'14px'} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{templateType === TemplateTypeEnum.teamPlugin && !searchKey && parentId && (
|
||||
<Flex alignItems={'center'} mt={2}>
|
||||
<FolderPath paths={paths} FirstPathDom={null} onClick={setParentId} />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
<RenderList
|
||||
templates={templates}
|
||||
onClose={onClose}
|
||||
parentId={parentId}
|
||||
setParentId={setParentId}
|
||||
/>
|
||||
</MyBox>
|
||||
</>
|
||||
);
|
||||
}, [isOpen, onClose, loading, t, templateType, searchKey, parentId, paths, templates, router]);
|
||||
|
||||
return Render;
|
||||
};
|
||||
|
||||
export default React.memo(NodeTemplatesModal);
|
||||
@@ -248,8 +228,8 @@ export default React.memo(NodeTemplatesModal);
|
||||
const RenderList = React.memo(function RenderList({
|
||||
templates,
|
||||
onClose,
|
||||
currentParent,
|
||||
setCurrentParent
|
||||
parentId,
|
||||
setParentId
|
||||
}: RenderListProps) {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
@@ -270,7 +250,7 @@ const RenderList = React.memo(function RenderList({
|
||||
});
|
||||
return copy.filter((item) => item.list.length > 0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [templates, currentParent]);
|
||||
}, [templates, parentId]);
|
||||
|
||||
const onAddNode = useCallback(
|
||||
async ({ template, position }: { template: FlowNodeTemplateType; position: XYPosition }) => {
|
||||
@@ -281,7 +261,7 @@ const RenderList = React.memo(function RenderList({
|
||||
// get plugin preview module
|
||||
if (template.flowNodeType === FlowNodeTypeEnum.pluginModule) {
|
||||
setLoading(true);
|
||||
const res = await getPreviewPluginModule(template.id);
|
||||
const res = await getPreviewPluginNode({ appId: template.id });
|
||||
|
||||
setLoading(false);
|
||||
return res;
|
||||
@@ -375,7 +355,7 @@ const RenderList = React.memo(function RenderList({
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'sm'}
|
||||
draggable={template.pluginType !== PluginTypeEnum.folder}
|
||||
draggable={template.pluginType !== AppTypeEnum.folder}
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < sliderWidth) return;
|
||||
onAddNode({
|
||||
@@ -384,11 +364,11 @@ const RenderList = React.memo(function RenderList({
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (template.pluginType === PluginTypeEnum.folder) {
|
||||
return setCurrentParent({
|
||||
parentId: template.id,
|
||||
parentName: template.name
|
||||
});
|
||||
if (
|
||||
template.pluginType === AppTypeEnum.folder ||
|
||||
template.pluginType === AppTypeEnum.httpPlugin
|
||||
) {
|
||||
return setParentId(template.id);
|
||||
}
|
||||
if (isPc) {
|
||||
return onAddNode({
|
||||
@@ -421,7 +401,7 @@ const RenderList = React.memo(function RenderList({
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}, [appT, formatTemplates, isPc, onAddNode, onClose, setCurrentParent, t, templates.length]);
|
||||
}, [appT, formatTemplates, isPc, onAddNode, onClose, setParentId, t, templates.length]);
|
||||
|
||||
return Render;
|
||||
});
|
||||
@@ -93,7 +93,7 @@ const ButtonEdge = (props: EdgeProps) => {
|
||||
if (highlightEdge) return '#3370ff';
|
||||
return '#94B5FF';
|
||||
}
|
||||
console.log(targetEdge);
|
||||
|
||||
// debug mode
|
||||
const colorMap = {
|
||||
[RuntimeEdgeStatusEnum.active]: '#39CC83',
|
||||
@@ -272,3 +272,7 @@ export const useDebug = () => {
|
||||
openDebugNode
|
||||
};
|
||||
};
|
||||
|
||||
export default function Dom() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { WorkflowContext, getWorkflowStore } from '../../context';
|
||||
|
||||
export const useKeyboard = () => {
|
||||
const { t } = useTranslation();
|
||||
const setNodes = useContextSelector(WorkflowContext, (v) => v.setNodes);
|
||||
const { setNodes, onSaveWorkflow } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const [isDowningCtrl, setIsDowningCtrl] = useState(false);
|
||||
@@ -81,6 +81,7 @@ export const useKeyboard = () => {
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setIsDowningCtrl(true);
|
||||
|
||||
switch (event.key) {
|
||||
case 'c':
|
||||
onCopy();
|
||||
@@ -88,12 +89,17 @@ export const useKeyboard = () => {
|
||||
case 'v':
|
||||
onParse();
|
||||
break;
|
||||
case 's':
|
||||
event.preventDefault();
|
||||
|
||||
onSaveWorkflow();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[onCopy, onParse]
|
||||
[onCopy, onParse, onSaveWorkflow]
|
||||
);
|
||||
|
||||
const handleKeyUp = useCallback((event: KeyboardEvent) => {
|
||||
@@ -118,3 +124,7 @@ export const useKeyboard = () => {
|
||||
isDowningCtrl
|
||||
};
|
||||
};
|
||||
|
||||
export default function Dom() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Connection, NodeChange, OnConnectStartParams, addEdge, EdgeChange, Edge } from 'reactflow';
|
||||
import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import 'reactflow/dist/style.css';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useKeyboard } from './useKeyboard';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
|
||||
export const useWorkflow = () => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { openConfirm: onOpenConfirmDeleteNode, ConfirmModal: ConfirmDeleteModal } = useConfirm({
|
||||
content: t('core.module.Confirm Delete Node'),
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
const { setConnectingEdge, nodes, onNodesChange, setEdges, onEdgesChange, setHoverEdgeId } =
|
||||
useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
/* node */
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
for (const change of changes) {
|
||||
if (change.type === 'remove') {
|
||||
const node = nodes.find((n) => n.id === change.id);
|
||||
if (node && node.data.forbidDelete) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.workflow.Can not delete node')
|
||||
});
|
||||
} else {
|
||||
return onOpenConfirmDeleteNode(() => {
|
||||
onNodesChange(changes);
|
||||
setEdges((state) =>
|
||||
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
|
||||
);
|
||||
})();
|
||||
}
|
||||
} else if (change.type === 'select' && change.selected === false && isDowningCtrl) {
|
||||
change.selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
onNodesChange(changes);
|
||||
},
|
||||
[isDowningCtrl, nodes, onNodesChange, onOpenConfirmDeleteNode, setEdges, t, toast]
|
||||
);
|
||||
const handleEdgeChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
onEdgesChange(changes.filter((change) => change.type !== 'remove'));
|
||||
},
|
||||
[onEdgesChange]
|
||||
);
|
||||
|
||||
/* connect */
|
||||
const onConnectStart = useCallback(
|
||||
(event: any, params: OnConnectStartParams) => {
|
||||
setConnectingEdge(params);
|
||||
},
|
||||
[setConnectingEdge]
|
||||
);
|
||||
const onConnectEnd = useCallback(() => {
|
||||
setConnectingEdge(undefined);
|
||||
}, [setConnectingEdge]);
|
||||
const onConnect = useCallback(
|
||||
({ connect }: { connect: Connection }) => {
|
||||
setEdges((state) =>
|
||||
addEdge(
|
||||
{
|
||||
...connect,
|
||||
type: EDGE_TYPE
|
||||
},
|
||||
state
|
||||
)
|
||||
);
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
const customOnConnect = useCallback(
|
||||
(connect: Connection) => {
|
||||
if (!connect.sourceHandle || !connect.targetHandle) {
|
||||
return;
|
||||
}
|
||||
if (connect.source === connect.target) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('core.module.Can not connect self')
|
||||
});
|
||||
}
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
},
|
||||
[onConnect, t, toast]
|
||||
);
|
||||
|
||||
/* edge */
|
||||
const onEdgeMouseEnter = useCallback(
|
||||
(e: any, edge: Edge) => {
|
||||
setHoverEdgeId(edge.id);
|
||||
},
|
||||
[setHoverEdgeId]
|
||||
);
|
||||
const onEdgeMouseLeave = useCallback(() => {
|
||||
setHoverEdgeId(undefined);
|
||||
}, [setHoverEdgeId]);
|
||||
|
||||
return {
|
||||
ConfirmDeleteModal,
|
||||
handleNodesChange,
|
||||
handleEdgeChange,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
onConnect,
|
||||
customOnConnect,
|
||||
onEdgeMouseEnter,
|
||||
onEdgeMouseLeave
|
||||
};
|
||||
};
|
||||
|
||||
export default function Dom() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
ControlButton,
|
||||
MiniMap,
|
||||
NodeProps,
|
||||
ReactFlowProvider,
|
||||
useReactFlow
|
||||
} from 'reactflow';
|
||||
import { Box, IconButton, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import ButtonEdge from './components/ButtonEdge';
|
||||
import NodeTemplatesModal from './NodeTemplatesModal';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { connectionLineStyle, defaultEdgeOptions } from '../constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useWorkflow } from './hooks/useWorkflow';
|
||||
|
||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
[FlowNodeTypeEnum.emptyNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.globalVariable]: NodeSimple,
|
||||
[FlowNodeTypeEnum.systemConfig]: dynamic(() => import('./nodes/NodeSystemConfig')),
|
||||
[FlowNodeTypeEnum.workflowStart]: dynamic(() => import('./nodes/NodeWorkflowStart')),
|
||||
[FlowNodeTypeEnum.chatNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.datasetSearchNode]: NodeSimple,
|
||||
[FlowNodeTypeEnum.datasetConcatNode]: dynamic(() => import('./nodes/NodeDatasetConcat')),
|
||||
[FlowNodeTypeEnum.answerNode]: dynamic(() => import('./nodes/NodeAnswer')),
|
||||
[FlowNodeTypeEnum.classifyQuestion]: dynamic(() => import('./nodes/NodeCQNode')),
|
||||
[FlowNodeTypeEnum.contentExtract]: dynamic(() => import('./nodes/NodeExtract')),
|
||||
[FlowNodeTypeEnum.httpRequest468]: dynamic(() => import('./nodes/NodeHttp')),
|
||||
[FlowNodeTypeEnum.runApp]: NodeSimple,
|
||||
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./nodes/NodePluginInput')),
|
||||
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./nodes/NodePluginOutput')),
|
||||
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
|
||||
[FlowNodeTypeEnum.queryExtension]: NodeSimple,
|
||||
[FlowNodeTypeEnum.tools]: dynamic(() => import('./nodes/NodeTools')),
|
||||
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowNodeItemType>) => (
|
||||
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
|
||||
),
|
||||
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
|
||||
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
|
||||
[FlowNodeTypeEnum.variableUpdate]: dynamic(() => import('./nodes/NodeVariableUpdate')),
|
||||
[FlowNodeTypeEnum.code]: dynamic(() => import('./nodes/NodeCode'))
|
||||
};
|
||||
const edgeTypes = {
|
||||
[EDGE_TYPE]: ButtonEdge
|
||||
};
|
||||
|
||||
const Workflow = () => {
|
||||
const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const {
|
||||
ConfirmDeleteModal,
|
||||
handleNodesChange,
|
||||
handleEdgeChange,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
customOnConnect,
|
||||
onEdgeMouseEnter,
|
||||
onEdgeMouseLeave
|
||||
} = useWorkflow();
|
||||
|
||||
const {
|
||||
isOpen: isOpenTemplate,
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
w={'100%'}
|
||||
position={'relative'}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* open module template */}
|
||||
<>
|
||||
<IconButton
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
left={5}
|
||||
size={'mdSquare'}
|
||||
borderRadius={'50%'}
|
||||
icon={<SmallCloseIcon fontSize={'26px'} />}
|
||||
transform={isOpenTemplate ? '' : 'rotate(135deg)'}
|
||||
transition={'0.2s ease'}
|
||||
aria-label={''}
|
||||
zIndex={1}
|
||||
boxShadow={'2px 2px 6px #85b1ff'}
|
||||
onClick={() => {
|
||||
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
|
||||
}}
|
||||
/>
|
||||
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
|
||||
</>
|
||||
|
||||
<ReactFlow
|
||||
ref={reactFlowWrapper}
|
||||
fitView
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.1}
|
||||
maxZoom={1.5}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
elevateEdgesOnSelect
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgeChange}
|
||||
onConnect={customOnConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
>
|
||||
<FlowController />
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
|
||||
<ConfirmDeleteModal />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Workflow);
|
||||
|
||||
const FlowController = React.memo(function FlowController() {
|
||||
const { fitView } = useReactFlow();
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<MiniMap
|
||||
style={{
|
||||
height: 78,
|
||||
width: 126,
|
||||
marginBottom: 35
|
||||
}}
|
||||
pannable
|
||||
/>
|
||||
<Controls
|
||||
position={'bottom-right'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
marginBottom: 5,
|
||||
background: 'white',
|
||||
borderRadius: '6px',
|
||||
overflow: 'hidden',
|
||||
boxShadow:
|
||||
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
|
||||
}}
|
||||
showInteractive={false}
|
||||
showFitView={false}
|
||||
>
|
||||
<MyTooltip label={'页面居中'}>
|
||||
<ControlButton className="custom-workflow-fix_view" onClick={() => fitView()}>
|
||||
<MyIcon name={'core/modules/fixview'} w={'14px'} />
|
||||
</ControlButton>
|
||||
</MyTooltip>
|
||||
</Controls>
|
||||
<Background />
|
||||
</>
|
||||
);
|
||||
}, [fitView]);
|
||||
|
||||
return Render;
|
||||
});
|
||||
@@ -38,7 +38,7 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
|
||||
|
||||
@@ -30,7 +30,7 @@ import { SourceHandle } from '../render/Handle';
|
||||
import { Position, useReactFlow } from 'reactflow';
|
||||
import { getRefData } from '@/web/core/workflow/utils';
|
||||
import DragIcon from '@fastgpt/web/components/common/DndDrag/DragIcon';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import { AppContext } from '@/pages/app/detail/components/context';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
|
||||
const ListItem = ({
|
||||
@@ -8,8 +8,8 @@ import { WorkflowIOValueTypeEnum, NodeInputKeyEnum } from '@fastgpt/global/core/
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getLafAppDetail } from '@/web/support/laf/api';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { getApiSchemaByUrl } from '@/web/core/plugin/api';
|
||||
import { getType, str2OpenApiSchema } from '@fastgpt/global/core/plugin/httpPlugin/utils';
|
||||
import { getApiSchemaByUrl } from '@/web/core/app/api/plugin';
|
||||
import { getType, str2OpenApiSchema } from '@fastgpt/global/core/app/httpPlugin/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { ChevronRightIcon } from '@chakra-ui/icons';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user