mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 21:13:50 +00:00
perf: http body;perf: create by json;perf: create by curl (#3570)
* perf: http body * feat: create app by json (#3557) * feat: create app by json * fix build * perf: create by json;perf: create by curl * fix: ts --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -10,7 +10,6 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bany/curl-to-json": "^1.2.8",
|
||||
"@chakra-ui/anatomy": "2.2.1",
|
||||
"@chakra-ui/icons": "2.1.1",
|
||||
"@chakra-ui/next-js": "2.1.5",
|
||||
|
136
projects/app/src/pageComponents/app/ImportAppConfigEditor.tsx
Normal file
136
projects/app/src/pageComponents/app/ImportAppConfigEditor.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React, { DragEvent, useCallback, useState } from 'react';
|
||||
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
const ImportAppConfigEditor = ({ value, onChange, rows = 16 }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const { File, onOpen } = useSelectFile({
|
||||
fileType: 'json',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const handleDragEnter = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const readJSONFile = useCallback(
|
||||
(file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (!file.name.endsWith('.json')) {
|
||||
toast({
|
||||
title: t('app:not_json_file'),
|
||||
status: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.target) {
|
||||
const res = JSON.parse(e.target.result as string);
|
||||
onChange(JSON.stringify(res, null, 2));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
},
|
||||
[onChange, t, toast]
|
||||
);
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
readJSONFile(file);
|
||||
},
|
||||
[readJSONFile]
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
console.log(file);
|
||||
readJSONFile(file);
|
||||
setIsDragging(false);
|
||||
},
|
||||
[readJSONFile]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box w={['100%', '31rem']}>
|
||||
{isDragging ? (
|
||||
<Flex
|
||||
align={'center'}
|
||||
justify={'center'}
|
||||
w={'full'}
|
||||
h={'17.5rem'}
|
||||
borderRadius={'md'}
|
||||
border={'1px dashed'}
|
||||
borderColor={'myGray.400'}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<Flex align={'center'} justify={'center'} flexDir={'column'} gap={'0.62rem'}>
|
||||
<MyIcon name={'configmap'} w={'2rem'} color={'primary.500'} />
|
||||
<Box color={'primary.600'} fontSize={'sm'}>
|
||||
{t('app:file_recover')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box>
|
||||
<Flex justify={'space-between'} align={'center'} pb={3}>
|
||||
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'500'}>
|
||||
{t('common:common.json_config')}
|
||||
</Box>
|
||||
<Button onClick={onOpen} variant={'whiteBase'} p={0}>
|
||||
<Flex px={'0.88rem'} py={'0.44rem'} color={'myGray.600'} fontSize={'mini'}>
|
||||
<MyIcon name={'file/uploadFile'} w={'1rem'} mr={'0.38rem'} />
|
||||
{t('common:common.upload_file')}
|
||||
</Flex>
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
border={'1px solid'}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.200'}
|
||||
value={value}
|
||||
placeholder={t('app:or_drag_JSON')}
|
||||
rows={rows}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{File && <File onSelect={onSelectFile} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ImportAppConfigEditor);
|
23
projects/app/src/pageComponents/app/constants.ts
Normal file
23
projects/app/src/pageComponents/app/constants.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
|
||||
export const appTypeMap = {
|
||||
[AppTypeEnum.simple]: {
|
||||
icon: 'core/app/simpleBot',
|
||||
title: i18nT('app:type.Create simple bot'),
|
||||
avatar: 'core/app/type/simpleFill',
|
||||
emptyCreateText: i18nT('app:create_empty_app')
|
||||
},
|
||||
[AppTypeEnum.workflow]: {
|
||||
icon: 'core/app/type/workflowFill',
|
||||
avatar: 'core/app/type/workflowFill',
|
||||
title: i18nT('app:type.Create workflow bot'),
|
||||
emptyCreateText: i18nT('app:create_empty_workflow')
|
||||
},
|
||||
[AppTypeEnum.plugin]: {
|
||||
icon: 'core/app/type/pluginFill',
|
||||
avatar: 'core/app/type/pluginFill',
|
||||
title: i18nT('app:type.Create plugin bot'),
|
||||
emptyCreateText: i18nT('app:create_empty_plugin')
|
||||
}
|
||||
};
|
@@ -0,0 +1,126 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { filterSensitiveNodesData } from '@/web/core/workflow/utils';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import { fileDownload } from '@/web/common/file/utils';
|
||||
import { AppChatConfigType, AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { filterSensitiveFormData } from '@/web/core/app/utils';
|
||||
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
|
||||
const ExportConfigPopover = ({
|
||||
appForm,
|
||||
getWorkflowData,
|
||||
|
||||
chatConfig,
|
||||
appName
|
||||
}: {
|
||||
appName: string;
|
||||
chatConfig?: AppChatConfigType;
|
||||
} & RequireOnlyOne<{
|
||||
getWorkflowData: () =>
|
||||
| {
|
||||
nodes: StoreNodeItemType[];
|
||||
edges: StoreEdgeItemType[];
|
||||
}
|
||||
| undefined;
|
||||
appForm: AppSimpleEditFormType;
|
||||
}>) => {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const onExportWorkflow = useCallback(
|
||||
async (mode: 'copy' | 'json') => {
|
||||
let config = '';
|
||||
|
||||
if (appForm) {
|
||||
config = JSON.stringify(filterSensitiveFormData(appForm), null, 2);
|
||||
} else if (getWorkflowData) {
|
||||
const workflowData = getWorkflowData();
|
||||
if (!workflowData) return;
|
||||
config = JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(workflowData.nodes),
|
||||
edges: workflowData.edges,
|
||||
chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'copy') {
|
||||
copyData(config, t('app:export_config_successful'));
|
||||
} else if (mode === 'json') {
|
||||
fileDownload({
|
||||
text: config,
|
||||
type: 'application/json;charset=utf-8',
|
||||
filename: `${appName}.json`
|
||||
});
|
||||
}
|
||||
},
|
||||
[appForm, appName, chatConfig, copyData, getWorkflowData, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyPopover
|
||||
placement={'right-start'}
|
||||
offset={[0, 20]}
|
||||
hasArrow
|
||||
trigger={'hover'}
|
||||
w={'8.6rem'}
|
||||
Trigger={
|
||||
<MyBox display={'flex'} cursor={'pointer'}>
|
||||
<MyIcon name={'export'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('app:export_configs')}</Box>
|
||||
</MyBox>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Box p={1}>
|
||||
<Flex
|
||||
py={'0.38rem'}
|
||||
px={1}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
bg: 'myGray.05',
|
||||
color: 'primary.600',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={() => onExportWorkflow('copy')}
|
||||
>
|
||||
<MyIcon name={'copy'} w={'1rem'} mr={2} />
|
||||
<Box fontSize={'mini'}>{t('common:common.copy_to_clipboard')}</Box>
|
||||
</Flex>
|
||||
<Flex
|
||||
py={'0.38rem'}
|
||||
px={1}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
bg: 'myGray.05',
|
||||
color: 'primary.600',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={() => onExportWorkflow('json')}
|
||||
>
|
||||
<MyIcon name={'configmap'} w={'1rem'} mr={2} />
|
||||
<Box fontSize={'mini'}>{t('common:common.export_to_json')}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</MyPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ExportConfigPopover);
|
@@ -24,6 +24,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postTransition2Workflow } from '@/web/core/app/api/app';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { SimpleAppSnapshotType } from './useSnapshots';
|
||||
import ExportConfigPopover from '@/pageComponents/app/detail/ExportConfigPopover';
|
||||
|
||||
const AppCard = ({
|
||||
appForm,
|
||||
@@ -118,6 +119,7 @@ const AppCard = ({
|
||||
)}
|
||||
{appDetail.permission.isOwner && (
|
||||
<MyMenu
|
||||
size={'xs'}
|
||||
Button={
|
||||
<IconButton
|
||||
variant={'whitePrimary'}
|
||||
@@ -129,6 +131,17 @@ const AppCard = ({
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: (
|
||||
<Flex>
|
||||
<ExportConfigPopover
|
||||
appName={appDetail.name}
|
||||
appForm={appForm}
|
||||
chatConfig={appDetail.chatConfig}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
},
|
||||
{
|
||||
icon: 'core/app/type/workflow',
|
||||
label: t('app:transition_to_workflow'),
|
||||
|
@@ -6,27 +6,27 @@ import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { WorkflowContext } from './context';
|
||||
import { filterSensitiveNodesData } from '@/web/core/workflow/utils';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { publishStatusStyle } from '../constants';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import { fileDownload } from '@/web/common/file/utils';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
|
||||
const ExportConfigPopover = dynamic(
|
||||
() => import('@/pageComponents/app/detail/ExportConfigPopover')
|
||||
);
|
||||
|
||||
const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp } = useContextSelector(
|
||||
AppContext,
|
||||
(v) => v
|
||||
);
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit);
|
||||
const onOpenTeamTagModal = useContextSelector(AppContext, (v) => v.onOpenTeamTagModal);
|
||||
const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp);
|
||||
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
|
||||
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
|
||||
@@ -92,10 +92,11 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved
|
||||
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
|
||||
cursor={'pointer'}
|
||||
>
|
||||
{ExportPopover({
|
||||
chatConfig: appDetail.chatConfig,
|
||||
appName: appDetail.name
|
||||
})}
|
||||
<ExportConfigPopover
|
||||
chatConfig={appDetail.chatConfig}
|
||||
appName={appDetail.name}
|
||||
getWorkflowData={flowData2StoreData}
|
||||
/>
|
||||
</MyBox>
|
||||
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
|
||||
<>
|
||||
@@ -148,6 +149,7 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved
|
||||
appDetail.permission.hasWritePer,
|
||||
appDetail.permission.isOwner,
|
||||
feConfigs?.show_team_chat,
|
||||
flowData2StoreData,
|
||||
onDelApp,
|
||||
onOpenImport,
|
||||
onOpenInfoEdit,
|
||||
@@ -210,104 +212,4 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved
|
||||
return Render;
|
||||
};
|
||||
|
||||
function ExportPopover({
|
||||
chatConfig,
|
||||
appName
|
||||
}: {
|
||||
chatConfig: AppChatConfigType;
|
||||
appName: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
|
||||
|
||||
const onExportWorkflow = useCallback(
|
||||
async (mode: 'copy' | 'json') => {
|
||||
const data = flowData2StoreData();
|
||||
if (data) {
|
||||
if (mode === 'copy') {
|
||||
copyData(
|
||||
JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
t('app:export_config_successful')
|
||||
);
|
||||
} else if (mode === 'json') {
|
||||
fileDownload({
|
||||
text: JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
type: 'application/json;charset=utf-8',
|
||||
filename: `${appName}.json`
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[appName, chatConfig, copyData, flowData2StoreData, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyPopover
|
||||
placement={'right-start'}
|
||||
offset={[0, 20]}
|
||||
hasArrow
|
||||
trigger={'hover'}
|
||||
w={'8.6rem'}
|
||||
Trigger={
|
||||
<MyBox display={'flex'} size={'md'} rounded={'4px'} cursor={'pointer'}>
|
||||
<MyIcon name={'export'} w={'16px'} mr={2} />
|
||||
<Box fontSize={'sm'}>{t('app:export_configs')}</Box>
|
||||
</MyBox>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Box p={1}>
|
||||
<Flex
|
||||
py={'0.38rem'}
|
||||
px={1}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
bg: 'myGray.05',
|
||||
color: 'primary.600',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={() => onExportWorkflow('copy')}
|
||||
>
|
||||
<MyIcon name={'copy'} w={'1rem'} mr={2} />
|
||||
<Box fontSize={'mini'}>{t('common:common.copy_to_clipboard')}</Box>
|
||||
</Flex>
|
||||
<Flex
|
||||
py={'0.38rem'}
|
||||
px={1}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
bg: 'myGray.05',
|
||||
color: 'primary.600',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={() => onExportWorkflow('json')}
|
||||
>
|
||||
<MyIcon name={'configmap'} w={'1rem'} mr={2} />
|
||||
<Box fontSize={'mini'}>{t('common:common.export_to_json')}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</MyPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppCard;
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import React, { DragEvent, useCallback, useMemo, useState } from 'react';
|
||||
import { Textarea, Button, ModalBody, ModalFooter, Flex, Box } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const ImportAppConfigEditor = dynamic(() => import('@/pageComponents/app/ImportAppConfigEditor'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
@@ -15,63 +17,10 @@ type Props = {
|
||||
|
||||
const ImportSettings = ({ onClose }: Props) => {
|
||||
const { toast } = useToast();
|
||||
const { File, onOpen } = useSelectFile({
|
||||
fileType: 'json',
|
||||
multiple: false
|
||||
});
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const readJSONFile = useCallback(
|
||||
(file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (!file.name.endsWith('.json')) {
|
||||
toast({
|
||||
title: t('app:not_json_file'),
|
||||
status: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.target) {
|
||||
const res = JSON.parse(e.target.result as string);
|
||||
setValue(JSON.stringify(res, null, 2));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
},
|
||||
[t, toast]
|
||||
);
|
||||
|
||||
const handleDragEnter = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
const handleDrop = useCallback(
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
readJSONFile(file);
|
||||
setIsDragging(false);
|
||||
},
|
||||
[readJSONFile]
|
||||
);
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
readJSONFile(file);
|
||||
},
|
||||
[readJSONFile]
|
||||
);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
@@ -80,67 +29,10 @@ const ImportSettings = ({ onClose }: Props) => {
|
||||
iconSrc="common/importLight"
|
||||
iconColor="primary.600"
|
||||
title={t('app:import_configs')}
|
||||
size={isPc ? 'lg' : 'md'}
|
||||
size={'md'}
|
||||
>
|
||||
<ModalBody>
|
||||
<File onSelect={onSelectFile} />
|
||||
{isDragging ? (
|
||||
<Flex
|
||||
align={'center'}
|
||||
justify={'center'}
|
||||
w={'31rem'}
|
||||
h={'17.5rem'}
|
||||
borderRadius={'md'}
|
||||
border={'1px dashed'}
|
||||
borderColor={'myGray.400'}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<Flex align={'center'} justify={'center'} flexDir={'column'} gap={'0.62rem'}>
|
||||
<MyIcon name={'configmap'} w={'1.5rem'} color={'myGray.500'} />
|
||||
<Box color={'myGray.600'} fontSize={'mini'}>
|
||||
{t('app:file_recover')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box w={['100%', '31rem']}>
|
||||
<Flex justify={'space-between'} align={'center'} pb={2}>
|
||||
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'500'}>
|
||||
{t('common:common.json_config')}
|
||||
</Box>
|
||||
<Button onClick={onOpen} variant={'whiteBase'} p={0}>
|
||||
<Flex px={'0.88rem'} py={'0.44rem'} color={'myGray.600'} fontSize={'mini'}>
|
||||
<MyIcon name={'file/uploadFile'} w={'1rem'} mr={'0.38rem'} />
|
||||
{t('common:common.upload_file')}
|
||||
</Flex>
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
border={'1px solid'}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.200'}
|
||||
value={value}
|
||||
placeholder={
|
||||
isPc
|
||||
? t('app:paste_config') + '\n' + t('app:or_drag_JSON')
|
||||
: t('app:paste_config')
|
||||
}
|
||||
rows={16}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<ImportAppConfigEditor value={value} onChange={setValue} rows={16} />
|
||||
</ModalBody>
|
||||
<ModalFooter justifyItems={'flex-end'}>
|
||||
<Button
|
||||
|
@@ -1,23 +1,14 @@
|
||||
import React from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { ModalBody, Button, ModalFooter, useDisclosure, Textarea, Box } from '@chakra-ui/react';
|
||||
import { ModalBody, Button, ModalFooter, Textarea } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import parse from '@bany/curl-to-json';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
|
||||
type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
|
||||
const methodMap: { [K in RequestMethod]: string } = {
|
||||
get: 'GET',
|
||||
post: 'POST',
|
||||
put: 'PUT',
|
||||
delete: 'DELETE',
|
||||
patch: 'PATCH'
|
||||
};
|
||||
import { parseCurl } from '@fastgpt/global/common/string/http';
|
||||
|
||||
const CurlImportModal = ({
|
||||
nodeId,
|
||||
@@ -49,22 +40,7 @@ const CurlImportModal = ({
|
||||
|
||||
if (!requestUrl || !requestMethod || !params || !headers || !jsonBody) return;
|
||||
|
||||
const parsed = parse(content);
|
||||
if (!parsed.url) {
|
||||
throw new Error('url not found');
|
||||
}
|
||||
|
||||
const newParams = Object.keys(parsed.params || {}).map((key) => ({
|
||||
key,
|
||||
value: parsed.params?.[key],
|
||||
type: 'string'
|
||||
}));
|
||||
const newHeaders = Object.keys(parsed.header || {}).map((key) => ({
|
||||
key,
|
||||
value: parsed.header?.[key],
|
||||
type: 'string'
|
||||
}));
|
||||
const newBody = JSON.stringify(parsed.data, null, 2);
|
||||
const parsed = parseCurl(content);
|
||||
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
@@ -82,7 +58,7 @@ const CurlImportModal = ({
|
||||
key: NodeInputKeyEnum.httpMethod,
|
||||
value: {
|
||||
...requestMethod,
|
||||
value: methodMap[parsed.method?.toLowerCase() as RequestMethod] || 'GET'
|
||||
value: parsed.method
|
||||
}
|
||||
});
|
||||
|
||||
@@ -92,7 +68,7 @@ const CurlImportModal = ({
|
||||
key: NodeInputKeyEnum.httpParams,
|
||||
value: {
|
||||
...params,
|
||||
value: newParams
|
||||
value: parsed.params
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,7 +78,7 @@ const CurlImportModal = ({
|
||||
key: NodeInputKeyEnum.httpHeaders,
|
||||
value: {
|
||||
...headers,
|
||||
value: newHeaders
|
||||
value: parsed.headers
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,7 +88,7 @@ const CurlImportModal = ({
|
||||
key: NodeInputKeyEnum.httpJsonBody,
|
||||
value: {
|
||||
...jsonBody,
|
||||
value: newBody
|
||||
value: parsed.body
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,10 +1,20 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Box, Flex, Button, ModalBody, Input, Grid, Card } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
ModalBody,
|
||||
Input,
|
||||
Grid,
|
||||
Card,
|
||||
Textarea,
|
||||
ModalFooter
|
||||
} from '@chakra-ui/react';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { useRouter } from 'next/router';
|
||||
import { emptyTemplates } from '@/web/core/app/templates';
|
||||
import { emptyTemplates, parsePluginFromCurlString } from '@/web/core/app/templates';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
@@ -21,10 +31,13 @@ import {
|
||||
getTemplateMarketItemList
|
||||
} from '@/web/core/app/api/template';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import { appTypeMap } from '@/pageComponents/app/constants';
|
||||
|
||||
type FormType = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
curlContent: string;
|
||||
};
|
||||
|
||||
export type CreateAppType = AppTypeEnum.simple | AppTypeEnum.workflow | AppTypeEnum.plugin;
|
||||
@@ -44,27 +57,10 @@ const CreateModal = ({
|
||||
const { isPc } = useSystem();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const typeMap = useRef({
|
||||
[AppTypeEnum.simple]: {
|
||||
icon: 'core/app/simpleBot',
|
||||
title: t('app:type.Create simple bot'),
|
||||
avatar: 'core/app/type/simpleFill',
|
||||
emptyCreateText: t('app:create_empty_app')
|
||||
},
|
||||
[AppTypeEnum.workflow]: {
|
||||
icon: 'core/app/type/workflowFill',
|
||||
avatar: 'core/app/type/workflowFill',
|
||||
title: t('app:type.Create workflow bot'),
|
||||
emptyCreateText: t('app:create_empty_workflow')
|
||||
},
|
||||
[AppTypeEnum.plugin]: {
|
||||
icon: 'core/app/type/pluginFill',
|
||||
avatar: 'core/app/type/pluginFill',
|
||||
title: t('app:type.Create plugin bot'),
|
||||
emptyCreateText: t('app:create_empty_plugin')
|
||||
}
|
||||
});
|
||||
const typeData = typeMap.current[type];
|
||||
const [currentCreateType, setCurrentCreateType] = useState<'template' | 'curl'>('template');
|
||||
const isTemplateMode = currentCreateType === 'template';
|
||||
|
||||
const typeData = appTypeMap[type];
|
||||
const { data: templateList = [], loading: isRequestTemplates } = useRequest2(
|
||||
() => getTemplateMarketItemList({ isQuickTemplate: true, type }),
|
||||
{
|
||||
@@ -75,11 +71,12 @@ const CreateModal = ({
|
||||
const { register, setValue, watch, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
avatar: typeData.avatar,
|
||||
name: ''
|
||||
name: '',
|
||||
curlContent: ''
|
||||
}
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
|
||||
const avatar = watch('avatar');
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
@@ -90,12 +87,13 @@ const CreateModal = ({
|
||||
});
|
||||
|
||||
const { runAsync: onclickCreate, loading: isCreating } = useRequest2(
|
||||
async (data: FormType, templateId?: string) => {
|
||||
if (!templateId) {
|
||||
async ({ avatar, name, curlContent }: FormType, templateId?: string) => {
|
||||
// From empty template
|
||||
if (!templateId && currentCreateType !== 'curl') {
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: data.avatar,
|
||||
name: data.name,
|
||||
avatar: avatar,
|
||||
name: name,
|
||||
type,
|
||||
modules: emptyTemplates[type].nodes,
|
||||
edges: emptyTemplates[type].edges,
|
||||
@@ -103,15 +101,31 @@ const CreateModal = ({
|
||||
});
|
||||
}
|
||||
|
||||
const templateDetail = await getTemplateMarketItemDetail(templateId);
|
||||
const { workflow, appAvatar } = await (async () => {
|
||||
if (templateId) {
|
||||
const templateDetail = await getTemplateMarketItemDetail(templateId);
|
||||
return {
|
||||
appAvatar: templateDetail.avatar,
|
||||
workflow: templateDetail.workflow
|
||||
};
|
||||
}
|
||||
if (curlContent) {
|
||||
return {
|
||||
appAvatar: avatar,
|
||||
workflow: parsePluginFromCurlString(curlContent)
|
||||
};
|
||||
}
|
||||
return Promise.reject('No template or curl content');
|
||||
})();
|
||||
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: data.avatar || templateDetail.avatar,
|
||||
name: data.name,
|
||||
type: templateDetail.type as AppTypeEnum,
|
||||
modules: templateDetail.workflow.nodes || [],
|
||||
edges: templateDetail.workflow.edges || [],
|
||||
chatConfig: templateDetail.workflow.chatConfig
|
||||
avatar: appAvatar,
|
||||
name: name,
|
||||
type,
|
||||
modules: workflow.nodes || [],
|
||||
edges: workflow.edges || [],
|
||||
chatConfig: workflow.chatConfig || {}
|
||||
});
|
||||
},
|
||||
{
|
||||
@@ -128,14 +142,13 @@ const CreateModal = ({
|
||||
return (
|
||||
<MyModal
|
||||
iconSrc={typeData.icon}
|
||||
title={typeData.title}
|
||||
title={t(typeData.title)}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
isCentered={!isPc}
|
||||
maxW={['90vw', '40rem']}
|
||||
isLoading={isCreating || isRequestTemplates}
|
||||
>
|
||||
<ModalBody px={9} pb={8}>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
@@ -161,111 +174,151 @@ const CreateModal = ({
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={[4, 7]} mb={[0, 3]}>
|
||||
<Box color={'myGray.900'} fontWeight={'bold'} fontSize={'sm'}>
|
||||
{t('common:core.app.Select app from template')}
|
||||
</Box>
|
||||
<Box flex={1} />
|
||||
<Flex
|
||||
onClick={() => onOpenTemplateModal(type)}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
fontSize={'xs'}
|
||||
_hover={{ color: 'blue.700' }}
|
||||
>
|
||||
{t('common:core.app.more')}
|
||||
<ChevronRightIcon w={4} h={4} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Grid
|
||||
userSelect={'none'}
|
||||
gridTemplateColumns={templateList.length > 0 ? ['repeat(1,1fr)', 'repeat(2,1fr)'] : '1fr'}
|
||||
gridGap={[2, 4]}
|
||||
>
|
||||
<Card
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
boxShadow={'3'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
color={'myGray.500'}
|
||||
borderColor={'myGray.200'}
|
||||
h={'8.25rem'}
|
||||
_hover={{
|
||||
color: 'primary.700',
|
||||
borderColor: 'primary.300'
|
||||
}}
|
||||
onClick={handleSubmit((data) => onclickCreate(data))}
|
||||
>
|
||||
<MyIcon name={'common/addLight'} w={'1.5rem'} />
|
||||
<Box fontSize={'sm'} mt={2}>
|
||||
{typeData.emptyCreateText}
|
||||
|
||||
<Flex mt={[4, 7]} mb={3}>
|
||||
{type === AppTypeEnum.plugin ? (
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{ label: t('app:create_by_template'), value: 'template' },
|
||||
{ label: t('app:create_by_curl'), value: 'curl' }
|
||||
]}
|
||||
value={currentCreateType}
|
||||
fontSize={'xs'}
|
||||
onChange={(e) => setCurrentCreateType(e as 'template' | 'curl')}
|
||||
/>
|
||||
) : (
|
||||
<Box color={'myGray.900'} fontWeight={'bold'} fontSize={'sm'}>
|
||||
{t('app:create_by_template')}
|
||||
</Box>
|
||||
</Card>
|
||||
{templateList.map((item) => (
|
||||
)}
|
||||
<Box flex={1} />
|
||||
{isTemplateMode && (
|
||||
<Flex
|
||||
onClick={() => onOpenTemplateModal(type)}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
fontSize={'xs'}
|
||||
_hover={{ color: 'blue.700' }}
|
||||
>
|
||||
{t('common:core.app.more')}
|
||||
<ChevronRightIcon w={4} h={4} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{isTemplateMode ? (
|
||||
<Grid
|
||||
userSelect={'none'}
|
||||
gridTemplateColumns={
|
||||
templateList.length > 0 ? ['repeat(1,1fr)', 'repeat(2,1fr)'] : '1fr'
|
||||
}
|
||||
gridGap={[2, 4]}
|
||||
>
|
||||
<Card
|
||||
key={item.templateId}
|
||||
p={4}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'myGray.200'}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
boxShadow={'3'}
|
||||
h={'8.25rem'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
'& .buttons': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
color={'myGray.500'}
|
||||
borderColor={'myGray.200'}
|
||||
h={'8.25rem'}
|
||||
_hover={{
|
||||
color: 'primary.700',
|
||||
borderColor: 'primary.300'
|
||||
}}
|
||||
onClick={handleSubmit((data) => onclickCreate(data))}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} />
|
||||
<Box ml={3} color={'myGray.900'} fontWeight={500}>
|
||||
{t(item.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} mt={2} color={'myGray.600'} flex={1}>
|
||||
{t(item.intro as any)}
|
||||
</Box>
|
||||
<Box w={'full'} fontSize={'mini'}>
|
||||
<Box color={'myGray.500'}>{`By ${item.author || feConfigs.systemTitle}`}</Box>
|
||||
<Box
|
||||
className="buttons"
|
||||
display={'none'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
borderRadius={'lg'}
|
||||
w={'full'}
|
||||
h={'full'}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={1}
|
||||
height={'40px'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
h={6}
|
||||
borderRadius={'sm'}
|
||||
w={'40%'}
|
||||
onClick={handleSubmit((data) => onclickCreate(data, item.templateId))}
|
||||
>
|
||||
{t('app:templateMarket.Use')}
|
||||
</Button>
|
||||
</Box>
|
||||
<MyIcon name={'common/addLight'} w={'1.5rem'} />
|
||||
<Box fontSize={'sm'} mt={2}>
|
||||
{t(typeData.emptyCreateText)}
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
{templateList.map((item) => (
|
||||
<Card
|
||||
key={item.templateId}
|
||||
p={4}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'3'}
|
||||
h={'8.25rem'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
'& .buttons': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} />
|
||||
<Box ml={3} color={'myGray.900'} fontWeight={500}>
|
||||
{t(item.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} mt={2} color={'myGray.600'} flex={1}>
|
||||
{t(item.intro as any)}
|
||||
</Box>
|
||||
<Box w={'full'} fontSize={'mini'}>
|
||||
<Box color={'myGray.500'}>{`By ${item.author || feConfigs.systemTitle}`}</Box>
|
||||
<Box
|
||||
className="buttons"
|
||||
display={'none'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
borderRadius={'lg'}
|
||||
w={'full'}
|
||||
h={'full'}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={1}
|
||||
height={'40px'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
h={6}
|
||||
borderRadius={'sm'}
|
||||
w={'40%'}
|
||||
onClick={handleSubmit((data) => onclickCreate(data, item.templateId))}
|
||||
>
|
||||
{t('app:templateMarket.Use')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Box>
|
||||
<Textarea
|
||||
placeholder={t('app:oaste_curl_string')}
|
||||
w={'560px'}
|
||||
h={'260px'}
|
||||
bg={'myGray.50'}
|
||||
{...register('curlContent')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter gap={4}>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button variant={'primary'} onClick={handleSubmit((data) => onclickCreate(data))}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
|
178
projects/app/src/pages/app/list/components/JsonImportModal.tsx
Normal file
178
projects/app/src/pages/app/list/components/JsonImportModal.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { appTypeMap } from '@/pageComponents/app/constants';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useMemo } from 'react';
|
||||
import { getAppType } from '@fastgpt/global/core/app/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { useRouter } from 'next/router';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import ImportAppConfigEditor from '@/pageComponents/app/ImportAppConfigEditor';
|
||||
|
||||
type FormType = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
workflowStr: string;
|
||||
};
|
||||
|
||||
const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
const router = useRouter();
|
||||
|
||||
const { register, setValue, watch, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
avatar: '',
|
||||
name: '',
|
||||
workflowStr: ''
|
||||
}
|
||||
});
|
||||
const workflowStr = watch('workflowStr');
|
||||
|
||||
const avatar = watch('avatar');
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
// If the user does not select an avatar, it will follow the type to change
|
||||
const selectedAvatar = useMemo(() => {
|
||||
if (avatar) return avatar;
|
||||
|
||||
const defaultVal = appTypeMap[AppTypeEnum.simple].avatar;
|
||||
if (!workflowStr) return defaultVal;
|
||||
|
||||
try {
|
||||
const workflow = JSON.parse(workflowStr);
|
||||
const type = getAppType(workflow);
|
||||
if (type) return appTypeMap[type].avatar;
|
||||
return defaultVal;
|
||||
} catch (err) {
|
||||
return defaultVal;
|
||||
}
|
||||
}, [avatar, workflowStr]);
|
||||
|
||||
const { runAsync: onSubmit, loading: isCreating } = useRequest2(
|
||||
async ({ name, workflowStr }: FormType) => {
|
||||
const { workflow, appType } = await (async () => {
|
||||
try {
|
||||
const workflow = JSON.parse(workflowStr);
|
||||
const appType = getAppType(workflow);
|
||||
|
||||
if (!appType) {
|
||||
return Promise.reject(t('app:type_not_recognized'));
|
||||
}
|
||||
|
||||
if (appType === AppTypeEnum.simple) {
|
||||
return {
|
||||
workflow: form2AppWorkflow(workflow, t),
|
||||
appType
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
workflow,
|
||||
appType
|
||||
};
|
||||
} catch (err) {
|
||||
return Promise.reject(t('app:invalid_json_format'));
|
||||
}
|
||||
})();
|
||||
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: selectedAvatar,
|
||||
name,
|
||||
type: appType,
|
||||
modules: workflow.nodes,
|
||||
edges: workflow.edges,
|
||||
chatConfig: workflow.chatConfig
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [selectedAvatar],
|
||||
onSuccess(id: string) {
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
loadMyApps();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Create Success')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
isLoading={isCreating}
|
||||
title={t('app:type.Import from json')}
|
||||
iconSrc="common/importLight"
|
||||
iconColor={'primary.600'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={2} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={selectedAvatar}
|
||||
w={['1.75rem', '2.25rem']}
|
||||
h={['1.75rem', '2.25rem']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={3}
|
||||
autoFocus
|
||||
bg={'myWhite.600'}
|
||||
{...register('name', {
|
||||
required: t('common:core.app.error.App name can not be empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Box mt={5}>
|
||||
<ImportAppConfigEditor
|
||||
value={workflowStr}
|
||||
onChange={(e) => setValue('workflowStr', e)}
|
||||
rows={10}
|
||||
/>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter gap={4}>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit(onSubmit)}>{t('common:common.Confirm')}</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
maxH: 300,
|
||||
maxW: 300,
|
||||
callback: (e) => setValue('avatar', e)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonImportModal;
|
@@ -37,6 +37,7 @@ import { webPushTrack } from '@/web/common/middle/tracks/utils';
|
||||
import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
|
||||
type TemplateAppType = AppTypeEnum | 'all';
|
||||
|
||||
@@ -94,14 +95,18 @@ const TemplateMarketModal = ({
|
||||
const { runAsync: onUseTemplate, loading: isCreating } = useRequest2(
|
||||
async (template: AppTemplateSchemaType) => {
|
||||
const templateDetail = await getTemplateMarketItemDetail(template.templateId);
|
||||
let workflow = templateDetail.workflow;
|
||||
if (templateDetail.type === AppTypeEnum.simple) {
|
||||
workflow = form2AppWorkflow(workflow, t);
|
||||
}
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: template.avatar,
|
||||
name: template.name,
|
||||
type: template.type as AppTypeEnum,
|
||||
modules: templateDetail.workflow.nodes || [],
|
||||
edges: templateDetail.workflow.edges || [],
|
||||
chatConfig: templateDetail.workflow.chatConfig
|
||||
modules: workflow.nodes || [],
|
||||
edges: workflow.edges || [],
|
||||
chatConfig: workflow.chatConfig
|
||||
}).then((res) => {
|
||||
webPushTrack.useAppTemplate({
|
||||
id: res,
|
||||
|
@@ -30,6 +30,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import TemplateMarketModal from './components/TemplateMarketModal';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import JsonImportModal from './components/JsonImportModal';
|
||||
|
||||
const CreateModal = dynamic(() => import('./components/CreateModal'));
|
||||
const EditFolderModal = dynamic(
|
||||
@@ -64,6 +65,11 @@ const MyApps = () => {
|
||||
onOpen: onOpenCreateHttpPlugin,
|
||||
onClose: onCloseCreateHttpPlugin
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenJsonImportModal,
|
||||
onOpen: onOpenJsonImportModal,
|
||||
onClose: onCloseJsonImportModal
|
||||
} = useDisclosure();
|
||||
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
|
||||
const [templateModalType, setTemplateModalType] = useState<AppTypeEnum | 'all'>();
|
||||
|
||||
@@ -244,6 +250,16 @@ const MyApps = () => {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/app/type/jsonImport',
|
||||
label: t('app:type.Import from json'),
|
||||
description: t('app:type.Import from json tip'),
|
||||
onClick: onOpenJsonImportModal
|
||||
}
|
||||
]
|
||||
},
|
||||
...(isPc
|
||||
? []
|
||||
: [
|
||||
@@ -375,6 +391,7 @@ const MyApps = () => {
|
||||
defaultType={templateModalType}
|
||||
/>
|
||||
)}
|
||||
{isOpenJsonImportModal && <JsonImportModal onClose={onCloseJsonImportModal} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@@ -74,7 +74,6 @@ export const useSelectFile = (props?: {
|
||||
maxW,
|
||||
maxH
|
||||
});
|
||||
console.log(src, '--');
|
||||
callback?.(src);
|
||||
return src;
|
||||
} catch (err: any) {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { AppItemType } from '@/types/app';
|
||||
import { parseCurl } from '@fastgpt/global/common/string/http';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import {
|
||||
@@ -408,3 +409,286 @@ export const emptyTemplates: Record<
|
||||
edges: []
|
||||
}
|
||||
};
|
||||
|
||||
export const parsePluginFromCurlString = (
|
||||
curl: string
|
||||
): {
|
||||
nodes: AppSchema['modules'];
|
||||
edges: AppSchema['edges'];
|
||||
chatConfig: AppSchema['chatConfig'];
|
||||
} => {
|
||||
const { url, method, headers, body } = parseCurl(curl);
|
||||
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
nodeId: 'pluginInput',
|
||||
name: 'workflow:template.plugin_start',
|
||||
intro: 'workflow:intro_plugin_input',
|
||||
avatar: 'core/workflow/template/workflowStart',
|
||||
flowNodeType: 'pluginInput',
|
||||
showStatus: false,
|
||||
position: {
|
||||
x: 630.1191328382079,
|
||||
y: -125.05298493910118
|
||||
},
|
||||
version: '481',
|
||||
inputs: [],
|
||||
outputs: []
|
||||
},
|
||||
{
|
||||
nodeId: 'pluginOutput',
|
||||
name: 'common:core.module.template.self_output',
|
||||
intro: 'workflow:intro_custom_plugin_output',
|
||||
avatar: 'core/workflow/template/pluginOutput',
|
||||
flowNodeType: 'pluginOutput',
|
||||
showStatus: false,
|
||||
position: {
|
||||
x: 1776.334576378706,
|
||||
y: -179.2671413906911
|
||||
},
|
||||
version: '481',
|
||||
inputs: [
|
||||
{
|
||||
renderTypeList: ['reference'],
|
||||
valueType: 'any',
|
||||
canEdit: true,
|
||||
key: 'result',
|
||||
label: 'result',
|
||||
isToolOutput: false,
|
||||
description: '',
|
||||
required: true,
|
||||
value: ['vumlECDQTjeC', 'httpRawResponse']
|
||||
},
|
||||
{
|
||||
renderTypeList: ['reference'],
|
||||
valueType: 'object',
|
||||
canEdit: true,
|
||||
key: 'error',
|
||||
label: 'error',
|
||||
isToolOutput: false,
|
||||
description: '',
|
||||
required: true,
|
||||
value: ['vumlECDQTjeC', 'error']
|
||||
}
|
||||
],
|
||||
outputs: []
|
||||
},
|
||||
{
|
||||
nodeId: 'vumlECDQTjeC',
|
||||
name: 'HTTP 请求',
|
||||
intro: '可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)',
|
||||
avatar: 'core/workflow/template/httpRequest',
|
||||
flowNodeType: 'httpRequest468',
|
||||
showStatus: true,
|
||||
position: {
|
||||
x: 1068.6226695001628,
|
||||
y: -435.2671413906911
|
||||
},
|
||||
version: '481',
|
||||
inputs: [
|
||||
{
|
||||
key: 'system_addInputParam',
|
||||
renderTypeList: ['addInputParam'],
|
||||
valueType: 'dynamic',
|
||||
label: '',
|
||||
required: false,
|
||||
description: '接收前方节点的输出值作为变量,这些变量可以被 HTTP 请求参数使用。',
|
||||
customInputConfig: {
|
||||
selectValueTypeList: [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'object',
|
||||
'arrayString',
|
||||
'arrayNumber',
|
||||
'arrayBoolean',
|
||||
'arrayObject',
|
||||
'arrayAny',
|
||||
'any',
|
||||
'chatHistory',
|
||||
'datasetQuote',
|
||||
'dynamic',
|
||||
'selectApp',
|
||||
'selectDataset'
|
||||
],
|
||||
showDescription: false,
|
||||
showDefaultValue: true
|
||||
},
|
||||
valueDesc: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
},
|
||||
{
|
||||
key: 'system_httpMethod',
|
||||
renderTypeList: ['custom'],
|
||||
valueType: 'string',
|
||||
label: '',
|
||||
value: method,
|
||||
required: true,
|
||||
valueDesc: '',
|
||||
description: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
},
|
||||
{
|
||||
key: 'system_httpTimeout',
|
||||
renderTypeList: ['custom'],
|
||||
valueType: 'number',
|
||||
label: '',
|
||||
value: 30,
|
||||
min: 5,
|
||||
max: 600,
|
||||
required: true,
|
||||
valueDesc: '',
|
||||
description: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
},
|
||||
{
|
||||
key: 'system_httpReqUrl',
|
||||
renderTypeList: ['hidden'],
|
||||
valueType: 'string',
|
||||
label: '',
|
||||
description:
|
||||
'新的 HTTP 请求地址。如果出现两个"请求地址",可以删除该模块重新加入,会拉取最新的模块配置。',
|
||||
placeholder: 'https://api.ai.com/getInventory',
|
||||
required: false,
|
||||
value: url,
|
||||
valueDesc: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
},
|
||||
{
|
||||
key: 'system_httpHeader',
|
||||
renderTypeList: ['custom'],
|
||||
valueType: 'any',
|
||||
value: headers,
|
||||
label: '',
|
||||
description:
|
||||
'自定义请求头,请严格填入 JSON 字符串。\n1. 确保最后一个属性没有逗号\n2. 确保 key 包含双引号\n例如:{"Authorization":"Bearer xxx"}',
|
||||
placeholder: 'common:core.module.input.description.Http Request Header',
|
||||
required: false,
|
||||
valueDesc: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
},
|
||||
{
|
||||
key: 'system_httpParams',
|
||||
renderTypeList: ['hidden'],
|
||||
valueType: 'any',
|
||||
description:
|
||||
'新的 HTTP 请求地址。如果出现两个“请求地址”,可以删除该模块重新加入,会拉取最新的模块配置。',
|
||||
label: '',
|
||||
required: false,
|
||||
valueDesc: '',
|
||||
description: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
},
|
||||
{
|
||||
key: 'system_httpJsonBody',
|
||||
renderTypeList: ['hidden'],
|
||||
valueType: 'any',
|
||||
value: body,
|
||||
label: '',
|
||||
required: false,
|
||||
valueDesc: '',
|
||||
description: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
},
|
||||
{
|
||||
key: 'system_httpFormBody',
|
||||
renderTypeList: ['hidden'],
|
||||
valueType: 'any',
|
||||
value: [],
|
||||
label: '',
|
||||
required: false,
|
||||
valueDesc: '',
|
||||
description: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
},
|
||||
{
|
||||
key: 'system_httpContentType',
|
||||
renderTypeList: ['hidden'],
|
||||
valueType: 'string',
|
||||
value: 'json',
|
||||
label: '',
|
||||
required: false,
|
||||
valueDesc: '',
|
||||
description: '',
|
||||
debugLabel: '',
|
||||
toolDescription: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'system_addOutputParam',
|
||||
key: 'system_addOutputParam',
|
||||
type: 'dynamic',
|
||||
valueType: 'dynamic',
|
||||
label: '输出字段提取',
|
||||
customFieldConfig: {
|
||||
selectValueTypeList: [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'object',
|
||||
'arrayString',
|
||||
'arrayNumber',
|
||||
'arrayBoolean',
|
||||
'arrayObject',
|
||||
'arrayAny',
|
||||
'any',
|
||||
'chatHistory',
|
||||
'datasetQuote',
|
||||
'dynamic',
|
||||
'selectApp',
|
||||
'selectDataset'
|
||||
],
|
||||
showDescription: false,
|
||||
showDefaultValue: false
|
||||
},
|
||||
description: '可以通过 JSONPath 语法来提取响应值中的指定字段',
|
||||
valueDesc: ''
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
key: 'error',
|
||||
label: '请求错误',
|
||||
description: 'HTTP请求错误信息,成功时返回空',
|
||||
valueType: 'object',
|
||||
type: 'static',
|
||||
valueDesc: ''
|
||||
},
|
||||
{
|
||||
id: 'httpRawResponse',
|
||||
key: 'httpRawResponse',
|
||||
required: true,
|
||||
label: '原始响应',
|
||||
description: 'HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。',
|
||||
valueType: 'any',
|
||||
type: 'static',
|
||||
valueDesc: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'pluginInput',
|
||||
target: 'vumlECDQTjeC',
|
||||
sourceHandle: 'pluginInput-source-right',
|
||||
targetHandle: 'vumlECDQTjeC-target-left'
|
||||
},
|
||||
{
|
||||
source: 'vumlECDQTjeC',
|
||||
target: 'pluginOutput',
|
||||
sourceHandle: 'vumlECDQTjeC-source-right',
|
||||
targetHandle: 'pluginOutput-target-left'
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
@@ -40,6 +40,7 @@ import {
|
||||
Input_Template_UserChatInput
|
||||
} from '@fastgpt/global/core/workflow/template/input';
|
||||
import { workflowStartNodeId } from './constants';
|
||||
import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
|
||||
|
||||
type WorkflowType = {
|
||||
nodes: StoreNodeItemType[];
|
||||
@@ -515,6 +516,13 @@ export function form2AppWorkflow(
|
||||
chatConfig: data.chatConfig
|
||||
};
|
||||
}
|
||||
export function filterSensitiveFormData(appForm: AppSimpleEditFormType) {
|
||||
const defaultAppForm = getDefaultAppForm();
|
||||
return {
|
||||
...appForm,
|
||||
dataset: defaultAppForm.dataset
|
||||
};
|
||||
}
|
||||
|
||||
export const workflowSystemVariables: EditorVariablePickerType[] = [
|
||||
{
|
||||
|
Reference in New Issue
Block a user