mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-29 01:40:51 +00:00
App template market (#2337)
* feat: add app template market (#2012) * feat: add app template market * fix * fix * i18n * fix * perf: template market ux * perf: simple mode app ui * perf: tempalte modal ui * perf: tempalte market ui * perf: template position * feat: create app modal * regiter default app * perf: icon * change templates position (#2331) * change templates position * fix * perf: template market ux --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -15,9 +15,15 @@ type Props = {
|
||||
llmModelType?: `${LLMModelTypeEnum}`;
|
||||
defaultData: SettingAIDataType;
|
||||
onChange: (e: SettingAIDataType) => void;
|
||||
bg?: string;
|
||||
};
|
||||
|
||||
const SettingLLMModel = ({ llmModelType = LLMModelTypeEnum.all, defaultData, onChange }: Props) => {
|
||||
const SettingLLMModel = ({
|
||||
llmModelType = LLMModelTypeEnum.all,
|
||||
defaultData,
|
||||
onChange,
|
||||
bg = 'white'
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { llmModelList } = useSystemStore();
|
||||
|
||||
@@ -63,6 +69,7 @@ const SettingLLMModel = ({ llmModelType = LLMModelTypeEnum.all, defaultData, onC
|
||||
w={'100%'}
|
||||
justifyContent={'flex-start'}
|
||||
variant={'whiteFlow'}
|
||||
bg={bg}
|
||||
_active={{
|
||||
transform: 'none'
|
||||
}}
|
||||
|
@@ -22,6 +22,7 @@ const WelcomeTextConfig = (props: TextareaProps) => {
|
||||
mt={2}
|
||||
rows={6}
|
||||
fontSize={'sm'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('common:core.app.tip.welcomeTextTip')}
|
||||
{...props}
|
||||
/>
|
||||
|
@@ -22,10 +22,11 @@ export type CreateAppBody = {
|
||||
type?: AppTypeEnum;
|
||||
modules: AppSchema['modules'];
|
||||
edges?: AppSchema['edges'];
|
||||
chatConfig?: AppSchema['chatConfig'];
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<CreateAppBody>) {
|
||||
const { parentId, name, avatar, type, modules, edges } = req.body;
|
||||
const { parentId, name, avatar, type, modules, edges, chatConfig } = req.body;
|
||||
|
||||
if (!name || !type || !Array.isArray(modules)) {
|
||||
return Promise.reject(CommonErrEnum.inheritPermissionError);
|
||||
@@ -50,6 +51,7 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
|
||||
type,
|
||||
modules,
|
||||
edges,
|
||||
chatConfig,
|
||||
teamId,
|
||||
tmbId
|
||||
});
|
||||
@@ -67,6 +69,7 @@ export const onCreateApp = async ({
|
||||
type,
|
||||
modules,
|
||||
edges,
|
||||
chatConfig,
|
||||
teamId,
|
||||
tmbId,
|
||||
pluginData,
|
||||
@@ -78,6 +81,7 @@ export const onCreateApp = async ({
|
||||
type?: AppTypeEnum;
|
||||
modules?: AppSchema['modules'];
|
||||
edges?: AppSchema['edges'];
|
||||
chatConfig?: AppSchema['chatConfig'];
|
||||
intro?: string;
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
@@ -96,6 +100,7 @@ export const onCreateApp = async ({
|
||||
tmbId,
|
||||
modules,
|
||||
edges,
|
||||
chatConfig,
|
||||
type,
|
||||
version: 'v2',
|
||||
pluginData,
|
||||
@@ -111,7 +116,8 @@ export const onCreateApp = async ({
|
||||
{
|
||||
appId,
|
||||
nodes: modules,
|
||||
edges
|
||||
edges,
|
||||
chatConfig
|
||||
}
|
||||
],
|
||||
{ session }
|
||||
|
21
projects/app/src/pages/api/core/app/template/detail.ts
Normal file
21
projects/app/src/pages/api/core/app/template/detail.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { TemplateMarketItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { getTemplateMarketItemDetail } from '@/service/core/app/template';
|
||||
|
||||
type Props = {
|
||||
templateId: string;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<any>
|
||||
): Promise<TemplateMarketItemType | undefined> {
|
||||
await authCert({ req, authToken: true });
|
||||
const { templateId } = req.query as Props;
|
||||
|
||||
return getTemplateMarketItemDetail(templateId);
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
16
projects/app/src/pages/api/core/app/template/list.ts
Normal file
16
projects/app/src/pages/api/core/app/template/list.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { TemplateMarketListItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { getTemplateMarketItemList } from '@/service/core/app/template';
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<any>
|
||||
): Promise<TemplateMarketListItemType[]> {
|
||||
await authCert({ req, authToken: true });
|
||||
|
||||
return getTemplateMarketItemList();
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
@@ -30,7 +30,6 @@ const Edit = ({
|
||||
// show selected dataset
|
||||
useMount(() => {
|
||||
loadAllDatasets();
|
||||
|
||||
setAppForm(
|
||||
appWorkflow2Form({
|
||||
nodes: appDetail.modules,
|
||||
|
@@ -135,13 +135,14 @@ const EditForm = ({
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
|
||||
<FormLabel ml={2} flex={1}>
|
||||
{appT('ai_settings')}
|
||||
{t('app:ai_settings')}
|
||||
</FormLabel>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box {...LabelStyles}>{t('common:core.ai.Model')}</Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<SettingLLMModel
|
||||
bg="myGray.50"
|
||||
llmModelType={'all'}
|
||||
defaultData={{
|
||||
model: appForm.aiSettings.model,
|
||||
@@ -176,6 +177,7 @@ const EditForm = ({
|
||||
<Box mt={1}>
|
||||
<PromptEditor
|
||||
value={appForm.aiSettings.systemPrompt}
|
||||
bg={'myGray.50'}
|
||||
onChange={(text) => {
|
||||
startTst(() => {
|
||||
setAppForm((state) => ({
|
||||
|
@@ -52,7 +52,6 @@ const Header = ({
|
||||
|
||||
const isPublished = useMemo(() => {
|
||||
const data = form2AppWorkflow(appForm, t);
|
||||
|
||||
return compareWorkflow(
|
||||
{
|
||||
nodes: appDetail.modules,
|
||||
|
@@ -1,15 +1,5 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
Input,
|
||||
Grid,
|
||||
useTheme,
|
||||
Card
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { Box, Flex, Button, ModalFooter, ModalBody, Input, Grid, Card } from '@chakra-ui/react';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { compressImgFileAndUpload } from '@/web/common/file/controller';
|
||||
@@ -17,8 +7,8 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { useRouter } from 'next/router';
|
||||
import { simpleBotTemplates, workflowTemplates, pluginTemplates } from '@/web/core/app/templates';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { emptyTemplates } 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';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
@@ -27,20 +17,31 @@ import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import {
|
||||
getTemplateMarketItemDetail,
|
||||
getTemplateMarketItemList
|
||||
} from '@/web/core/app/api/template';
|
||||
|
||||
type FormType = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
templateId: string;
|
||||
};
|
||||
|
||||
export type CreateAppType = AppTypeEnum.simple | AppTypeEnum.workflow | AppTypeEnum.plugin;
|
||||
|
||||
const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => void }) => {
|
||||
const CreateModal = ({
|
||||
onClose,
|
||||
type,
|
||||
onOpenTemplateModal
|
||||
}: {
|
||||
type: CreateAppType;
|
||||
onClose: () => void;
|
||||
onOpenTemplateModal: (type: AppTypeEnum) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
@@ -49,34 +50,39 @@ const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => vo
|
||||
const typeMap = useRef({
|
||||
[AppTypeEnum.simple]: {
|
||||
icon: 'core/app/simpleBot',
|
||||
title: appT('type.Create simple bot'),
|
||||
title: t('app:type.Create simple bot'),
|
||||
avatar: '/imgs/app/avatar/simple.svg',
|
||||
templates: simpleBotTemplates
|
||||
emptyCreateText: t('app:create_empty_app')
|
||||
},
|
||||
[AppTypeEnum.workflow]: {
|
||||
icon: 'core/app/type/workflowFill',
|
||||
avatar: '/imgs/app/avatar/workflow.svg',
|
||||
title: appT('type.Create workflow bot'),
|
||||
templates: workflowTemplates
|
||||
title: t('app:type.Create workflow bot'),
|
||||
emptyCreateText: t('app:create_empty_workflow')
|
||||
},
|
||||
[AppTypeEnum.plugin]: {
|
||||
icon: 'core/app/type/pluginFill',
|
||||
avatar: '/imgs/app/avatar/plugin.svg',
|
||||
title: appT('type.Create plugin bot'),
|
||||
templates: pluginTemplates
|
||||
title: t('app:type.Create plugin bot'),
|
||||
emptyCreateText: t('app:create_empty_plugin')
|
||||
}
|
||||
});
|
||||
const typeData = typeMap.current[type];
|
||||
|
||||
const { data: templateList = [] } = useRequest2(getTemplateMarketItemList, {
|
||||
manual: false
|
||||
});
|
||||
const filterTemplates = useMemo(() => {
|
||||
return templateList.filter((item) => item.type === type).slice(0, 3);
|
||||
}, [templateList, type]);
|
||||
|
||||
const { register, setValue, watch, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
avatar: typeData.avatar,
|
||||
name: '',
|
||||
templateId: typeData.templates[0].id
|
||||
name: ''
|
||||
}
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
const templateId = watch('templateId');
|
||||
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
@@ -105,29 +111,42 @@ const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => vo
|
||||
[setValue, t, toast]
|
||||
);
|
||||
|
||||
const { mutate: onclickCreate, isLoading: creating } = useRequest({
|
||||
mutationFn: async (data: FormType) => {
|
||||
const template = typeData.templates.find((item) => item.id === data.templateId);
|
||||
if (!template) {
|
||||
return Promise.reject(t('common:core.dataset.error.Template does not exist'));
|
||||
const { runAsync: onclickCreate, loading: isCreating } = useRequest2(
|
||||
async (data: FormType, templateId?: string) => {
|
||||
if (!templateId) {
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: data.avatar,
|
||||
name: data.name,
|
||||
type,
|
||||
modules: emptyTemplates[type].nodes,
|
||||
edges: emptyTemplates[type].edges,
|
||||
chatConfig: emptyTemplates[type].chatConfig
|
||||
});
|
||||
}
|
||||
|
||||
const templateDetail = await getTemplateMarketItemDetail({ templateId: templateId });
|
||||
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: data.avatar || template.avatar,
|
||||
avatar: data.avatar || templateDetail.avatar,
|
||||
name: data.name,
|
||||
type: template.type,
|
||||
modules: template.modules || [],
|
||||
edges: template.edges || []
|
||||
type: templateDetail.type,
|
||||
modules: templateDetail.workflow.nodes || [],
|
||||
edges: templateDetail.workflow.edges || [],
|
||||
chatConfig: templateDetail.workflow.chatConfig
|
||||
});
|
||||
},
|
||||
onSuccess(id: string) {
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
loadMyApps();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
});
|
||||
{
|
||||
onSuccess(id: string) {
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
loadMyApps();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
@@ -136,8 +155,10 @@ const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => vo
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
isCentered={!isPc}
|
||||
maxW={['90vw', '40rem']}
|
||||
isLoading={isCreating}
|
||||
>
|
||||
<ModalBody>
|
||||
<ModalBody px={9} pb={8}>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
@@ -146,8 +167,8 @@ const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => vo
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={avatar}
|
||||
w={['28px', '32px']}
|
||||
h={['28px', '32px']}
|
||||
w={['28px', '36px']}
|
||||
h={['28px', '36px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
@@ -155,7 +176,7 @@ const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => vo
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={4}
|
||||
ml={3}
|
||||
autoFocus
|
||||
bg={'myWhite.600'}
|
||||
{...register('name', {
|
||||
@@ -163,59 +184,111 @@ const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => vo
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Box mt={[4, 7]} mb={[0, 3]} color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('common:core.app.Select app from template')}
|
||||
</Box>
|
||||
<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={['repeat(1,1fr)', 'repeat(2,1fr)']}
|
||||
gridGap={[2, 4]}
|
||||
>
|
||||
{typeData.templates.map((item) => (
|
||||
<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}
|
||||
</Box>
|
||||
</Card>
|
||||
{filterTemplates.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
border={'base'}
|
||||
p={3}
|
||||
p={4}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
boxShadow={'sm'}
|
||||
{...(templateId === item.id
|
||||
? {
|
||||
bg: 'primary.50',
|
||||
borderColor: 'primary.500'
|
||||
}
|
||||
: {
|
||||
_hover: {
|
||||
boxShadow: 'md'
|
||||
}
|
||||
})}
|
||||
onClick={() => {
|
||||
setValue('templateId', item.id);
|
||||
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'}>
|
||||
<Box ml={3} color={'myGray.900'} fontWeight={500}>
|
||||
{t(item.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize={'xs'} mt={2} color={'myGray.600'}>
|
||||
<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}</Box>
|
||||
<Box
|
||||
className="buttons"
|
||||
display={'none'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
borderRadius={'lg'}
|
||||
w={'full'}
|
||||
h={'full'}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
height={'40px'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
h={'1.75rem'}
|
||||
borderRadius={'xl'}
|
||||
w={'40%'}
|
||||
onClick={handleSubmit((data) => onclickCreate(data, item.id))}
|
||||
>
|
||||
{t('app:templateMarket.Use')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button px={6} isLoading={creating} onClick={handleSubmit((data) => onclickCreate(data))}>
|
||||
{t('common:common.Confirm Create')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<File onSelect={onSelectFile} />
|
||||
</MyModal>
|
||||
);
|
||||
|
@@ -0,0 +1,452 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useCallback, useState } from 'react';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import AppTypeTag from './TypeTag';
|
||||
import { AppTemplateTypeEnum, AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import {
|
||||
getTemplateMarketItemDetail,
|
||||
getTemplateMarketItemList
|
||||
} from '@/web/core/app/api/template';
|
||||
import { TemplateMarketListItemType } from '@fastgpt/global/core/workflow/type';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
import { useRouter } from 'next/router';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import SearchInput from '../../../../../../../packages/web/components/common/Input/SearchInput/index';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
type TemplateAppType = AppTypeEnum | 'all';
|
||||
|
||||
const TemplateMarketModal = ({
|
||||
defaultType = 'all',
|
||||
onClose
|
||||
}: {
|
||||
defaultType?: TemplateAppType;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const templateTags = [
|
||||
{
|
||||
id: AppTemplateTypeEnum.recommendation,
|
||||
label: t('app:templateMarket.templateTags.Recommendation')
|
||||
},
|
||||
{
|
||||
id: AppTemplateTypeEnum.writing,
|
||||
label: t('app:templateMarket.templateTags.Writing')
|
||||
},
|
||||
{
|
||||
id: AppTemplateTypeEnum.imageGeneration,
|
||||
label: t('app:templateMarket.templateTags.Image_generation')
|
||||
},
|
||||
{
|
||||
id: AppTemplateTypeEnum.webSearch,
|
||||
label: t('app:templateMarket.templateTags.Web_search')
|
||||
},
|
||||
{
|
||||
id: AppTemplateTypeEnum.roleplay,
|
||||
label: t('app:templateMarket.templateTags.Roleplay')
|
||||
},
|
||||
{
|
||||
id: AppTemplateTypeEnum.officeServices,
|
||||
label: t('app:templateMarket.templateTags.Office_services')
|
||||
}
|
||||
];
|
||||
|
||||
const { parentId } = useContextSelector(AppListContext, (v) => v);
|
||||
const router = useRouter();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const [currentTag, setCurrentTag] = useState(templateTags[0].id);
|
||||
const [currentAppType, setCurrentAppType] = useState<TemplateAppType>(defaultType);
|
||||
const [currentSearch, setCurrentSearch] = useState('');
|
||||
|
||||
const { data: templateList = [], loading: isLoadingTemplates } = useRequest2(
|
||||
getTemplateMarketItemList,
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onUseTemplate, loading: isCreating } = useRequest2(
|
||||
async (id: string) => {
|
||||
const templateDetail = await getTemplateMarketItemDetail({ templateId: id });
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: templateDetail.avatar,
|
||||
name: templateDetail.name,
|
||||
type: templateDetail.type,
|
||||
modules: templateDetail.workflow.nodes || [],
|
||||
edges: templateDetail.workflow.edges || [],
|
||||
chatConfig: templateDetail.workflow.chatConfig
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess(id: string) {
|
||||
onClose();
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { run: handleScroll } = useRequest2(
|
||||
async () => {
|
||||
let firstVisibleTitle: any = null;
|
||||
|
||||
templateTags
|
||||
.map((type) => type.id)
|
||||
.forEach((type: string) => {
|
||||
const element = document.getElementById(type);
|
||||
if (!element) return;
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
if (elementRect.top <= window.innerHeight && elementRect.bottom >= 0) {
|
||||
if (
|
||||
!firstVisibleTitle ||
|
||||
elementRect.top < firstVisibleTitle.getBoundingClientRect().top
|
||||
) {
|
||||
firstVisibleTitle = element;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (firstVisibleTitle) {
|
||||
setCurrentTag(firstVisibleTitle.id);
|
||||
}
|
||||
},
|
||||
{
|
||||
throttleWait: 100
|
||||
}
|
||||
);
|
||||
|
||||
const TemplateCard = useCallback(
|
||||
({ item }: { item: TemplateMarketListItemType }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
key={item.id}
|
||||
lineHeight={1.5}
|
||||
h="100%"
|
||||
pt={4}
|
||||
pb={3}
|
||||
px={4}
|
||||
border={'base'}
|
||||
boxShadow={'2'}
|
||||
bg={'white'}
|
||||
borderRadius={'10px'}
|
||||
position={'relative'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
boxShadow: '1.5',
|
||||
'& .buttons': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} h={'1.5rem'} />
|
||||
<Box flex={'1 0 0'} color={'myGray.900'} fontWeight={500}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<Box mr={'-1rem'}>
|
||||
<AppTypeTag type={item.type} />
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box
|
||||
flex={['1 0 48px', '1 0 56px']}
|
||||
mt={3}
|
||||
pr={8}
|
||||
textAlign={'justify'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Box className={'textEllipsis2'}>{item.intro || t('app:templateMarket.no_intro')}</Box>
|
||||
</Box>
|
||||
|
||||
<Box w={'full'} fontSize={'mini'}>
|
||||
<Box color={'myGray.500'}>By {item.author}</Box>
|
||||
<Box
|
||||
className="buttons"
|
||||
display={'none'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
borderRadius={'lg'}
|
||||
w={'full'}
|
||||
h={'full'}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
height={'40px'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
h={'1.75rem'}
|
||||
borderRadius={'xl'}
|
||||
w={'40%'}
|
||||
onClick={() => onUseTemplate(item.id)}
|
||||
>
|
||||
{t('app:templateMarket.Use')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</MyBox>
|
||||
);
|
||||
},
|
||||
[onUseTemplate]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => onClose && onClose()}
|
||||
autoFocus={false}
|
||||
blockScrollOnMount={false}
|
||||
closeOnOverlayClick={false}
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
w={['90vw', '80vw']}
|
||||
maxW={'90vw'}
|
||||
position={'relative'}
|
||||
h={['90vh']}
|
||||
boxShadow={'7'}
|
||||
overflow={'hidden'}
|
||||
>
|
||||
<ModalHeader
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
py={'10px'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'600'}
|
||||
gap={2}
|
||||
position={'relative'}
|
||||
>
|
||||
<Avatar src={'/imgs/app/templateFill.svg'} w={'2rem'} objectFit={'fill'} />
|
||||
<Box color={'myGray.900'}>{t('app:templateMarket.Template_market')}</Box>
|
||||
|
||||
<Box flex={'1'} />
|
||||
|
||||
<MySelect
|
||||
h={'8'}
|
||||
value={currentAppType}
|
||||
onchange={(value) => {
|
||||
setCurrentAppType(value as AppTypeEnum | 'all');
|
||||
}}
|
||||
bg={'myGray.100'}
|
||||
minW={'7rem'}
|
||||
borderRadius={'sm'}
|
||||
list={[
|
||||
{ label: t('app:type.All'), value: 'all' },
|
||||
{ label: t('app:type.Simple bot'), value: AppTypeEnum.simple },
|
||||
{ label: t('app:type.Workflow bot'), value: AppTypeEnum.workflow },
|
||||
{ label: t('app:type.Plugin'), value: AppTypeEnum.plugin }
|
||||
]}
|
||||
/>
|
||||
<ModalCloseButton position={'relative'} fontSize={'xs'} top={0} right={0} />
|
||||
|
||||
{isPc && (
|
||||
<Box
|
||||
width="15rem"
|
||||
position={'absolute'}
|
||||
top={'50%'}
|
||||
left={'50%'}
|
||||
transform={'translate(-50%,-50%)'}
|
||||
>
|
||||
<SearchInput
|
||||
pl={7}
|
||||
placeholder={t('app:templateMarket.Search_template')}
|
||||
value={currentSearch}
|
||||
onChange={(e) => setCurrentSearch(e.target.value)}
|
||||
h={8}
|
||||
bg={'myGray.50'}
|
||||
maxLength={20}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<MyBox isLoading={isCreating || isLoadingTemplates} flex={'1 0 0'} overflow={'overlay'}>
|
||||
<ModalBody
|
||||
h={'100%'}
|
||||
display={'flex'}
|
||||
bg={'myGray.100'}
|
||||
overflow={'auto'}
|
||||
gap={5}
|
||||
onScroll={handleScroll}
|
||||
px={0}
|
||||
pt={5}
|
||||
>
|
||||
{isPc && (
|
||||
<Flex pl={5} flexDirection={'column'} gap={3}>
|
||||
{templateTags.map((item) => {
|
||||
return (
|
||||
<Box
|
||||
key={item.id}
|
||||
cursor={'pointer'}
|
||||
{...(item.id === currentTag && !currentSearch
|
||||
? {
|
||||
bg: 'primary.1',
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
_hover: { bg: 'primary.1' },
|
||||
color: 'myGray.600'
|
||||
})}
|
||||
w={'9.5rem'}
|
||||
px={4}
|
||||
py={2}
|
||||
rounded={'sm'}
|
||||
fontSize={'sm'}
|
||||
fontWeight={500}
|
||||
onClick={() => {
|
||||
setCurrentTag(item.id);
|
||||
const anchor = document.getElementById(item.id);
|
||||
if (anchor) {
|
||||
anchor.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
<Box flex={1} />
|
||||
|
||||
{feConfigs?.appTemplateCourse && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
py={2}
|
||||
fontWeight={500}
|
||||
rounded={'sm'}
|
||||
fontSize={'sm'}
|
||||
onClick={() => window.open(feConfigs.appTemplateCourse)}
|
||||
gap={1}
|
||||
>
|
||||
<MyIcon name={'common/upRightArrowLight'} w={'1rem'} />
|
||||
<Box>{t('common:contribute_app_template')}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Box pl={[3, 0]} pr={[3, 5]} pt={1} flex={'1'} h={'100%'} overflow={'auto'}>
|
||||
{currentSearch ? (
|
||||
<>
|
||||
<Box fontSize={'lg'} color={'myGray.900'} mb={4}>
|
||||
{t('common:xx_search_result', { key: currentSearch })}
|
||||
</Box>
|
||||
{(() => {
|
||||
const templates = templateList.filter((template) =>
|
||||
`${template.name}${template.intro}`.includes(currentSearch)
|
||||
);
|
||||
|
||||
if (templates.length > 0) {
|
||||
return (
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'1fr',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={4}
|
||||
alignItems={'stretch'}
|
||||
pb={5}
|
||||
>
|
||||
{templates.map((item) => (
|
||||
<TemplateCard key={item.id} item={item} />
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return <EmptyTip text={t('app:template_market_empty_data')} />;
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{templateTags.map((item) => {
|
||||
const currentTemplates = templateList
|
||||
?.filter((template) => template.tags.includes(item.id))
|
||||
.filter((template) => {
|
||||
if (currentAppType === 'all') return true;
|
||||
return template.type === currentAppType;
|
||||
});
|
||||
|
||||
if (currentTemplates.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box key={item.id}>
|
||||
<Box
|
||||
id={item.id}
|
||||
fontSize={['md', 'lg']}
|
||||
color={'myGray.900'}
|
||||
mb={4}
|
||||
fontWeight={500}
|
||||
>
|
||||
{item.label}
|
||||
</Box>
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'1fr',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={4}
|
||||
alignItems={'stretch'}
|
||||
pb={5}
|
||||
>
|
||||
{currentTemplates.map((item) => (
|
||||
<TemplateCard key={item.id} item={item} />
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyBox>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateMarketModal;
|
@@ -45,7 +45,7 @@ const AppTypeTag = ({ type }: { type: AppTypeEnum }) => {
|
||||
py={0.5}
|
||||
pl={2}
|
||||
pr={3}
|
||||
borderLeftRadius={'md'}
|
||||
borderLeftRadius={'sm'}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
<MyIcon name={data.icon as any} w={'0.8rem'} color={'myGray.500'} />
|
||||
|
@@ -42,6 +42,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import TemplateMarketModal from './components/TemplateMarketModal';
|
||||
|
||||
const CreateModal = dynamic(() => import('./components/CreateModal'));
|
||||
const EditFolderModal = dynamic(
|
||||
@@ -77,6 +78,7 @@ const MyApps = () => {
|
||||
onClose: onCloseCreateHttpPlugin
|
||||
} = useDisclosure();
|
||||
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
|
||||
const [templateModalType, setTemplateModalType] = useState<AppTypeEnum | 'all'>();
|
||||
|
||||
const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, {
|
||||
onSuccess() {
|
||||
@@ -145,19 +147,19 @@ const MyApps = () => {
|
||||
<LightRowTabs
|
||||
list={[
|
||||
{
|
||||
label: appT('type.All'),
|
||||
label: t('app:type.All'),
|
||||
value: 'ALL'
|
||||
},
|
||||
{
|
||||
label: appT('type.Simple bot'),
|
||||
label: t('app:type.Simple bot'),
|
||||
value: AppTypeEnum.simple
|
||||
},
|
||||
{
|
||||
label: appT('type.Workflow bot'),
|
||||
label: t('app:type.Workflow bot'),
|
||||
value: AppTypeEnum.workflow
|
||||
},
|
||||
{
|
||||
label: appT('type.Plugin'),
|
||||
label: t('app:type.Plugin'),
|
||||
value: AppTypeEnum.plugin
|
||||
}
|
||||
]}
|
||||
@@ -195,30 +197,40 @@ const MyApps = () => {
|
||||
children: [
|
||||
{
|
||||
icon: 'core/app/simpleBot',
|
||||
label: appT('type.Simple bot'),
|
||||
description: appT('type.Create simple bot tip'),
|
||||
label: t('app:type.Simple bot'),
|
||||
description: t('app:type.Create simple bot tip'),
|
||||
onClick: () => setCreateAppType(AppTypeEnum.simple)
|
||||
},
|
||||
{
|
||||
icon: 'core/app/type/workflowFill',
|
||||
label: appT('type.Workflow bot'),
|
||||
description: appT('type.Create workflow tip'),
|
||||
label: t('app:type.Workflow bot'),
|
||||
description: t('app:type.Create workflow tip'),
|
||||
onClick: () => setCreateAppType(AppTypeEnum.workflow)
|
||||
},
|
||||
{
|
||||
icon: 'core/app/type/pluginFill',
|
||||
label: appT('type.Plugin'),
|
||||
description: appT('type.Create one plugin tip'),
|
||||
label: t('app:type.Plugin'),
|
||||
description: t('app:type.Create one plugin tip'),
|
||||
onClick: () => setCreateAppType(AppTypeEnum.plugin)
|
||||
},
|
||||
{
|
||||
icon: 'core/app/type/httpPluginFill',
|
||||
label: appT('type.Http plugin'),
|
||||
description: appT('type.Create http plugin tip'),
|
||||
label: t('app:type.Http plugin'),
|
||||
description: t('app:type.Create http plugin tip'),
|
||||
onClick: onOpenCreateHttpPlugin
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: '/imgs/app/templateFill.svg',
|
||||
label: t('app:template_market'),
|
||||
description: t('app:template_market_description'),
|
||||
onClick: () => setTemplateModalType('all')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
@@ -306,9 +318,19 @@ const MyApps = () => {
|
||||
/>
|
||||
)}
|
||||
{!!createAppType && (
|
||||
<CreateModal type={createAppType} onClose={() => setCreateAppType(undefined)} />
|
||||
<CreateModal
|
||||
type={createAppType}
|
||||
onClose={() => setCreateAppType(undefined)}
|
||||
onOpenTemplateModal={setTemplateModalType}
|
||||
/>
|
||||
)}
|
||||
{isOpenCreateHttpPlugin && <HttpEditModal onClose={onCloseCreateHttpPlugin} />}
|
||||
{!!templateModalType && (
|
||||
<TemplateMarketModal
|
||||
onClose={() => setTemplateModalType(undefined)}
|
||||
defaultType={templateModalType}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@@ -7,9 +7,10 @@ import { useSendCode } from '@/web/support/user/hooks/useSendCode';
|
||||
import type { ResLogin } from '@/global/support/api/userRes';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { defaultAppTemplates } from '@/web/core/app/templates';
|
||||
import { emptyTemplates } from '@/web/core/app/templates';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
interface Props {
|
||||
loginSuccess: (e: ResLogin) => void;
|
||||
setPageType: Dispatch<`${LoginPageTypeEnum}`>;
|
||||
@@ -68,13 +69,13 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
});
|
||||
// auto register template app
|
||||
setTimeout(() => {
|
||||
defaultAppTemplates.forEach((template) => {
|
||||
Object.entries(emptyTemplates).map(([type, emptyTemplate]) => {
|
||||
postCreateApp({
|
||||
avatar: template.avatar,
|
||||
name: t(template.name as any),
|
||||
modules: template.modules,
|
||||
edges: template.edges,
|
||||
type: template.type
|
||||
avatar: emptyTemplate.avatar,
|
||||
name: t(emptyTemplate.name as any),
|
||||
modules: emptyTemplate.nodes,
|
||||
edges: emptyTemplate.edges,
|
||||
type: type as AppTypeEnum
|
||||
});
|
||||
});
|
||||
}, 100);
|
||||
|
@@ -62,6 +62,8 @@ const defaultFeConfigs: FastGPTFeConfigsType = {
|
||||
docUrl: 'https://doc.fastgpt.in',
|
||||
openAPIDocUrl: 'https://doc.fastgpt.in/docs/development/openapi',
|
||||
systemPluginCourseUrl: 'https://fael3z0zfze.feishu.cn/wiki/ERZnw9R26iRRG0kXZRec6WL9nwh',
|
||||
appTemplateCourse:
|
||||
'https://fael3z0zfze.feishu.cn/wiki/CX9wwMGyEi5TL6koiLYcg7U0nWb?fromScene=spaceOverview',
|
||||
systemTitle: 'FastGPT',
|
||||
concatMd:
|
||||
'项目开源地址: [FastGPT GitHub](https://github.com/labring/FastGPT)\n交流群: ',
|
||||
|
48
projects/app/src/service/core/app/template.ts
Normal file
48
projects/app/src/service/core/app/template.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { isProduction } from '@fastgpt/service/common/system/constants';
|
||||
import { readdirSync, readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Get template from memory or file system
|
||||
const loadTemplateMarketItems = async () => {
|
||||
if (isProduction && global.appMarketTemplates) return global.appMarketTemplates;
|
||||
|
||||
const templatesDir = path.join(process.cwd(), 'public', 'appMarketTemplates');
|
||||
const templateNames = readdirSync(templatesDir);
|
||||
|
||||
global.appMarketTemplates = templateNames.map((name) => {
|
||||
try {
|
||||
const filePath = path.join(templatesDir, name, 'template.json');
|
||||
const fileContent = readFileSync(filePath, 'utf-8');
|
||||
const data = JSON.parse(fileContent);
|
||||
return {
|
||||
id: name,
|
||||
...data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching template ${name}:`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
global.appMarketTemplates.sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0));
|
||||
|
||||
return global.appMarketTemplates;
|
||||
};
|
||||
|
||||
export const getTemplateMarketItemDetail = async (id: string) => {
|
||||
const templateMarketItems = await loadTemplateMarketItems();
|
||||
return templateMarketItems.find((item) => item.id === id);
|
||||
};
|
||||
|
||||
export const getTemplateMarketItemList = async () => {
|
||||
const templateMarketItems = await loadTemplateMarketItems();
|
||||
return templateMarketItems.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
avatar: item.avatar,
|
||||
intro: item.intro,
|
||||
author: item.author,
|
||||
tags: item.tags,
|
||||
type: item.type
|
||||
}));
|
||||
};
|
11
projects/app/src/web/core/app/api/template.ts
Normal file
11
projects/app/src/web/core/app/api/template.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { GET } from '@/web/common/api/request';
|
||||
import {
|
||||
TemplateMarketItemType,
|
||||
TemplateMarketListItemType
|
||||
} from '@fastgpt/global/core/workflow/type';
|
||||
|
||||
export const getTemplateMarketItemList = () =>
|
||||
GET<TemplateMarketListItemType[]>('/core/app/template/list');
|
||||
|
||||
export const getTemplateMarketItemDetail = (data: { templateId: string }) =>
|
||||
GET<TemplateMarketItemType>(`/core/app/template/detail`, data);
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user