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:
Archer
2024-08-12 16:21:21 +08:00
committed by GitHub
parent 231afc4ac5
commit 2196930005
58 changed files with 5934 additions and 3165 deletions

View File

@@ -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'
}}

View File

@@ -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}
/>

View File

@@ -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 }

View 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);

View 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);

View File

@@ -30,7 +30,6 @@ const Edit = ({
// show selected dataset
useMount(() => {
loadAllDatasets();
setAppForm(
appWorkflow2Form({
nodes: appDetail.modules,

View File

@@ -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) => ({

View File

@@ -52,7 +52,6 @@ const Header = ({
const isPublished = useMemo(() => {
const data = form2AppWorkflow(appForm, t);
return compareWorkflow(
{
nodes: appDetail.modules,

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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'} />

View File

@@ -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>
);
};

View File

@@ -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);

View File

@@ -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交流群: ![](https://oss.laf.run/htr4n1-images/fastgpt-qr-code.jpg)',

View 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
}));
};

View 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