Feat: App folder and permission (#1726)

* app folder

* feat: app foldere

* fix: run app param error

* perf: select app ux

* perf: folder rerender

* fix: ts

* fix: parentId

* fix: permission

* perf: loading ux

* perf: per select ux

* perf: clb context

* perf: query extension tip

* fix: ts

* perf: app detail per

* perf: default per
This commit is contained in:
Archer
2024-06-11 10:16:24 +08:00
committed by GitHub
parent b20d075d35
commit bc6864c3dc
89 changed files with 2495 additions and 695 deletions

View File

@@ -17,7 +17,6 @@ import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import dayjs from 'dayjs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import DateRangePicker, {
type DateRangeType
} from '@fastgpt/web/components/common/DateRangePicker';

View File

@@ -1,6 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import type { CreateAppParams } from '@/global/core/app/api.d';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
@@ -9,17 +8,24 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { AppSchema } from '@fastgpt/global/core/app/type';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const {
name = 'APP',
avatar,
type = AppTypeEnum.advanced,
modules,
edges
} = req.body as CreateAppParams;
export type CreateAppBody = {
parentId?: ParentIdType;
name?: string;
avatar?: string;
type?: AppTypeEnum;
modules: AppSchema['modules'];
edges?: AppSchema['edges'];
};
if (!name || !Array.isArray(modules)) {
async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse<any>) {
const { parentId, name, avatar, type, modules, edges } = req.body;
if (!name || !type || !Array.isArray(modules)) {
throw new Error('缺少参数');
}
@@ -34,6 +40,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const [{ _id: appId }] = await MongoApp.create(
[
{
...parseParentIdInMongo(parentId),
avatar,
name,
teamId,

View File

@@ -9,6 +9,7 @@ import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { NextAPI } from '@/service/middleware/entry';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import { findAppAndAllChildren } from '@fastgpt/service/core/app/controller';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
@@ -17,50 +18,61 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
throw new Error('参数错误');
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
// Auth owner (folder owner, can delete all apps in the folder)
const { teamId } = await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
const apps = await findAppAndAllChildren({
teamId,
appId,
fields: '_id'
});
console.log(apps);
// 删除对应的聊天
await mongoSessionRun(async (session) => {
await MongoChatItem.deleteMany(
{
appId
},
{ session }
);
await MongoChat.deleteMany(
{
appId
},
{ session }
);
// 删除分享链接
await MongoOutLink.deleteMany(
{
appId
},
{ session }
);
// delete version
await MongoAppVersion.deleteMany(
{
appId
},
{ session }
);
await MongoChatInputGuide.deleteMany(
{
appId
},
{ session }
);
// delete app
await MongoApp.deleteOne(
{
_id: appId
},
{ session }
);
for await (const app of apps) {
const appId = app._id;
// Chats
await MongoChatItem.deleteMany(
{
appId
},
{ session }
);
await MongoChat.deleteMany(
{
appId
},
{ session }
);
// 删除分享链接
await MongoOutLink.deleteMany(
{
appId
},
{ session }
);
// delete version
await MongoAppVersion.deleteMany(
{
appId
},
{ session }
);
await MongoChatInputGuide.deleteMany(
{
appId
},
{ session }
);
// delete app
await MongoApp.deleteOne(
{
_id: appId
},
{ session }
);
}
});
}

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
/* 获取我的模型 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -11,7 +11,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
throw new Error('参数错误');
}
// 凭证校验
const { app } = await authApp({ req, authToken: true, appId, per: WritePermissionVal });
const { app } = await authApp({ req, authToken: true, appId, per: ReadPermissionVal });
if (!app.permission.hasWritePer) {
app.modules = [];
app.edges = [];
}
return app;
}

View File

@@ -0,0 +1,40 @@
import type { NextApiResponse } from 'next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
import { NextAPI } from '@/service/middleware/entry';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
export type CreateAppFolderBody = {
parentId?: ParentIdType;
name: string;
intro?: string;
};
async function handler(req: ApiRequestProps<CreateAppFolderBody>, res: NextApiResponse<any>) {
const { name, intro, parentId } = req.body;
if (!name) {
throw new Error('缺少参数');
}
// 凭证校验
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
// Create app
await MongoApp.create({
...parseParentIdInMongo(parentId),
avatar: FolderImgUrl,
name,
intro,
teamId,
tmbId,
type: AppTypeEnum.folder
});
}
export default NextAPI(handler);

View File

@@ -0,0 +1,41 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type {
ParentIdType,
ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type.d';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
async function handler(
req: NextApiRequest,
res: NextApiResponse<any>
): Promise<ParentTreePathItemType[]> {
const { parentId } = req.query as { parentId: string };
if (!parentId) {
return [];
}
await authApp({ req, authToken: true, appId: parentId, per: ReadPermissionVal });
return await getParents(parentId);
}
export default NextAPI(handler);
async function getParents(parentId: ParentIdType): Promise<ParentTreePathItemType[]> {
if (!parentId) {
return [];
}
const parent = await MongoApp.findById(parentId, 'name parentId');
if (!parent) return [];
const paths = await getParents(parent.parentId);
paths.push({ parentId, parentName: parent.name });
return paths;
}

View File

@@ -1,4 +1,4 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiResponse } from 'next';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppListItemType } from '@fastgpt/global/core/app/type';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
@@ -9,8 +9,21 @@ import {
ReadPermissionVal
} from '@fastgpt/global/support/permission/constant';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<AppListItemType[]> {
export type ListAppBody = {
parentId: ParentIdType;
type?: AppTypeEnum;
};
async function handler(
req: ApiRequestProps<ListAppBody>,
res: NextApiResponse<any>
): Promise<AppListItemType[]> {
// 凭证校验
const {
teamId,
@@ -22,9 +35,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
per: ReadPermissionVal
});
const { parentId, type } = req.body;
/* temp: get all apps and per */
const [myApps, rpList] = await Promise.all([
MongoApp.find({ teamId }, '_id avatar name intro tmbId defaultPermission')
MongoApp.find(
{ teamId, ...(type && { type }), ...parseParentIdInMongo(parentId) },
'_id avatar type name intro tmbId defaultPermission'
)
.sort({
updateTime: -1
})
@@ -54,10 +72,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
return filterApps.map((app) => ({
_id: app._id,
avatar: app.avatar,
type: app.type,
name: app.name,
intro: app.intro,
permission: app.permission,
defaultPermission: app.defaultPermission
defaultPermission: app.defaultPermission || AppDefaultPermissionVal
}));
}

View File

@@ -9,10 +9,12 @@ import {
WritePermissionVal,
OwnerPermissionVal
} from '@fastgpt/global/support/permission/constant';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
/* 获取我的模型 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const {
parentId,
name,
avatar,
type,
@@ -49,6 +51,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
_id: appId
},
{
...parseParentIdInMongo(parentId),
name,
type,
avatar,

View File

@@ -92,7 +92,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
data: {
list: searchRes,
duration: `${((Date.now() - start) / 1000).toFixed(3)}s`,
usingQueryExtension: !!aiExtensionResult,
queryExtensionModel: aiExtensionResult?.model,
...result
}
});

View File

@@ -20,7 +20,7 @@ import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import MemberManager from '@/components/support/permission/MemberManager';
import { CollaboratorContextProvider } from '@/components/support/permission/MemberManager/context';
import {
postUpdateAppCollaborators,
deleteAppCollaborators,
@@ -29,12 +29,12 @@ import {
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import {
AppDefaultPermission,
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
@@ -181,25 +181,57 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
{/* role */}
{appDetail.permission.hasManagePer && (
<>
{' '}
<Box mt="4">
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
<DefaultPermissionList
mt="2"
per={defaultPermission}
defaultPer={AppDefaultPermission}
readPer={ReadPermissionVal}
writePer={WritePermissionVal}
defaultPer={AppDefaultPermissionVal}
onChange={(v) => setValue('defaultPermission', v)}
/>
</Box>
<Box mt={6}>
<MemberManager
<CollaboratorContextProvider
permission={appDetail.permission}
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
permissionList={AppPermissionList}
onUpdateCollaborators={onUpdateCollaborators}
onDelOneCollaborator={onDelCollaborator}
/>
>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>
<Flex
alignItems="center"
flexDirection="row"
justifyContent="space-between"
w="full"
>
<Box fontSize={'sm'}></Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('permission.Manage')}
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common.Add')}
</Button>
</Flex>
</Flex>
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
</>
);
}}
</CollaboratorContextProvider>
</Box>
</>
)}

View File

@@ -6,7 +6,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRouter } from 'next/router';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { delModelById } from '@/web/core/app/api';
import { delAppById } from '@/web/core/app/api';
import { useTranslation } from 'next-i18next';
import PermissionIconText from '@/components/support/permission/IconText';
import dynamic from 'next/dynamic';
@@ -45,7 +45,7 @@ const AppCard = () => {
const { mutate: handleDelModel, isLoading } = useRequest({
mutationFn: async () => {
if (!appDetail) return null;
await delModelById(appDetail._id);
await delAppById(appDetail._id);
return 'success';
},
onSuccess(res) {

View File

@@ -297,7 +297,7 @@ const EditForm = ({
similarity={getValues('dataset.similarity')}
limit={getValues('dataset.limit')}
usingReRank={getValues('dataset.usingReRank')}
usingQueryExtension={getValues('dataset.datasetSearchUsingExtensionQuery')}
queryExtensionModel={getValues('dataset.datasetSearchExtensionModel')}
/>
</Box>
)}

View File

@@ -25,6 +25,8 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { useContextSelector } from 'use-context-selector';
import { AppListContext } from './context';
type FormType = {
avatar: string;
@@ -32,12 +34,15 @@ type FormType = {
templateId: string;
};
const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) => {
const CreateModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const router = useRouter();
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
const theme = useTheme();
const { isPc, feConfigs } = useSystemStore();
const { isPc } = useSystemStore();
const { register, setValue, watch, handleSubmit } = useForm<FormType>({
defaultValues: {
avatar: '',
@@ -82,6 +87,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
return Promise.reject(t('core.dataset.error.Template does not exist'));
}
return postCreateApp({
parentId,
avatar: data.avatar || template.avatar,
name: data.name,
type: template.type,
@@ -91,7 +97,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
},
onSuccess(id: string) {
router.push(`/app/detail?appId=${id}`);
onSuccess();
loadMyApps();
onClose();
},
successToast: t('common.Create Success'),

View File

@@ -0,0 +1,294 @@
import React, { useMemo, useState } from 'react';
import { Box, Grid, Flex, IconButton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { delAppById, putAppById } from '@/web/core/app/api';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import PermissionIconText from '@/components/support/permission/IconText';
import { useI18n } from '@/web/context/I18n';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
import { AppListContext } from './context';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
import dynamic from 'next/dynamic';
import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import {
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import {
deleteAppCollaborators,
getCollaboratorList,
postUpdateAppCollaborators
} from '@/web/core/app/api/collaborator';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const ConfigPerModal = dynamic(() => import('@/components/support/permission/ConfigPerModal'));
const ListItem = () => {
const { t } = useTranslation();
const { appT } = useI18n();
const router = useRouter();
const { myApps, loadMyApps, onUpdateApp, setMoveAppId } = useContextSelector(
AppListContext,
(v) => v
);
const [loadingAppId, setLoadingAppId] = useState<string>();
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
const [editPerAppIndex, setEditPerAppIndex] = useState<number>();
const editPerApp = useMemo(
() => (editPerAppIndex !== undefined ? myApps[editPerAppIndex] : undefined),
[editPerAppIndex, myApps]
);
const { getBoxProps } = useFolderDrag({
activeStyles: {
borderColor: 'primary.600'
},
onDrop: async (dragId: string, targetId: string) => {
setLoadingAppId(dragId);
try {
await putAppById(dragId, { parentId: targetId });
loadMyApps();
} catch (error) {}
setLoadingAppId(undefined);
}
});
const { openConfirm, ConfirmModal } = useConfirm({
type: 'delete'
});
const { run: onclickDelApp } = useRequest2(
(id: string) => {
setLoadingAppId(id);
return delAppById(id);
},
{
onSuccess() {
loadMyApps();
},
onFinally() {
setLoadingAppId(undefined);
},
successToast: t('common.Delete Success'),
errorToast: t('common.Delete Failed')
}
);
return (
<>
<Grid
py={[4, 6]}
gridTemplateColumns={['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']}
gridGap={5}
>
{myApps.map((app, index) => (
<MyTooltip
key={app._id}
label={
app.type === AppTypeEnum.folder
? t('common.folder.Open folder')
: app.permission.hasWritePer
? appT('Edit app')
: appT('Go to chat')
}
>
<MyBox
isLoading={loadingAppId === app._id}
lineHeight={1.5}
h={'100%'}
py={3}
px={5}
cursor={'pointer'}
borderWidth={'1.5px'}
borderColor={'borderColor.low'}
bg={'white'}
borderRadius={'md'}
userSelect={'none'}
position={'relative'}
display={'flex'}
flexDirection={'column'}
_hover={{
borderColor: 'primary.300',
boxShadow: '1.5',
'& .more': {
display: 'flex'
},
'& .chat': {
display: 'flex'
}
}}
onClick={() => {
if (app.type === AppTypeEnum.folder) {
router.push({
query: {
parentId: app._id
}
});
} else if (app.permission.hasWritePer) {
router.push(`/app/detail?appId=${app._id}`);
} else {
router.push(`/chat?appId=${app._id}`);
}
}}
{...getBoxProps({
dataId: app._id,
isFolder: app.type === AppTypeEnum.folder
})}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={app.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3}>{app.name}</Box>
{app.permission.hasManagePer && (
<Box
className="more"
position={'absolute'}
top={3.5}
right={4}
display={['', 'none']}
>
<MyMenu
Button={
<IconButton
size={'xsSquare'}
variant={'transparentBase'}
icon={<MyIcon name={'more'} w={'1rem'} />}
aria-label={''}
/>
}
menuList={[
{
children: [
{
icon: 'edit',
label: '编辑信息',
onClick: () =>
setEditedApp({
id: app._id,
avatar: app.avatar,
name: app.name,
intro: app.intro
})
},
{
icon: 'common/file/move',
label: t('common.folder.Move to'),
onClick: () => setMoveAppId(app._id)
},
...(app.permission.hasManagePer
? [
{
icon: 'support/team/key',
label: t('permission.Permission'),
onClick: () => setEditPerAppIndex(index)
}
]
: [])
]
},
...(app.permission.isOwner
? [
{
children: [
{
type: 'danger' as 'danger',
icon: 'delete',
label: t('common.Delete'),
onClick: () =>
openConfirm(
() => onclickDelApp(app._id),
undefined,
app.type === AppTypeEnum.folder
? appT('Confirm delete folder tip')
: appT('Confirm Del App Tip')
)()
}
]
}
]
: [])
]}
/>
</Box>
)}
</Flex>
<Box
flex={1}
className={'textEllipsis3'}
py={2}
wordBreak={'break-all'}
fontSize={'mini'}
color={'myGray.600'}
>
{app.intro || '还没写介绍~'}
</Box>
<Flex h={'34px'} alignItems={'flex-end'}>
<Box flex={1}>
<PermissionIconText
defaultPermission={app.defaultPermission}
color={'myGray.600'}
/>
</Box>
</Flex>
</MyBox>
</MyTooltip>
))}
</Grid>
{myApps.length === 0 && <EmptyTip text={'还没有应用,快去创建一个吧!'} pt={'30vh'} />}
<ConfirmModal />
{!!editedApp && (
<EditResourceModal
{...editedApp}
title="应用信息编辑"
onClose={() => {
setEditedApp(undefined);
}}
onEdit={({ id, ...data }) => onUpdateApp(id, data)}
/>
)}
{!!editPerApp && (
<ConfigPerModal
avatar={editPerApp.avatar}
name={editPerApp.name}
defaultPer={{
value: editPerApp.defaultPermission,
defaultValue: AppDefaultPermissionVal,
onChange: (e) => {
return onUpdateApp(editPerApp._id, { defaultPermission: e });
}
}}
managePer={{
permission: editPerApp.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
permissionList: AppPermissionList,
onUpdateCollaborators: (tmbIds: string[], permission: number) => {
return postUpdateAppCollaborators({
tmbIds,
permission,
appId: editPerApp._id
});
},
onDelOneCollaborator: (tmbId: string) =>
deleteAppCollaborators({
appId: editPerApp._id,
tmbId
})
}}
onClose={() => setEditPerAppIndex(undefined)}
/>
)}
</>
);
};
export default ListItem;

View File

@@ -0,0 +1,138 @@
import React, { ReactNode, useCallback, useState } from 'react';
import { createContext } from 'use-context-selector';
import { useRouter } from 'next/router';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppDetailById, getMyApps, putAppById } from '@/web/core/app/api';
import { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type';
import { useQuery } from '@tanstack/react-query';
import { getAppFolderPath } from '@/web/core/app/api/app';
import {
GetResourceFolderListProps,
ParentIdType,
ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type';
import { AppUpdateParams } from '@/global/core/app/api';
import dynamic from 'next/dynamic';
import { useI18n } from '@/web/context/I18n';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { delay } from '@fastgpt/global/common/system/utils';
type AppListContextType = {
parentId?: string | null;
myApps: AppListItemType[];
loadMyApps: () => void;
isFetchingApps: boolean;
folderDetail: AppDetailType | undefined | null;
paths: ParentTreePathItemType[];
onUpdateApp: (id: string, data: AppUpdateParams) => Promise<any>;
setMoveAppId: React.Dispatch<React.SetStateAction<string | undefined>>;
};
export const AppListContext = createContext<AppListContextType>({
parentId: undefined,
myApps: [],
loadMyApps: function (): void {
throw new Error('Function not implemented.');
},
isFetchingApps: false,
folderDetail: undefined,
paths: [],
onUpdateApp: function (id: string, data: AppUpdateParams): Promise<any> {
throw new Error('Function not implemented.');
},
setMoveAppId: function (value: React.SetStateAction<string | undefined>): void {
throw new Error('Function not implemented.');
}
});
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
const AppListContextProvider = ({ children }: { children: ReactNode }) => {
const { appT } = useI18n();
const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const {
data = [],
runAsync: loadMyApps,
loading: isFetchingApps
} = useRequest2(() => getMyApps({ parentId }), {
manual: false,
refreshOnWindowFocus: true,
refreshDeps: [parentId]
});
const { data: paths = [], runAsync: refetchPaths } = useRequest2(
() => getAppFolderPath(parentId),
{
manual: false,
refreshDeps: [parentId]
}
);
const { data: folderDetail, runAsync: refetchFolderDetail } = useRequest2(
() => {
if (parentId) return getAppDetailById(parentId);
return Promise.resolve(null);
},
{
manual: false,
refreshDeps: [parentId]
}
);
const { runAsync: onUpdateApp } = useRequest2((id: string, data: AppUpdateParams) =>
putAppById(id, data).then(async (res) => {
await Promise.all([refetchFolderDetail(), refetchPaths(), loadMyApps()]);
return res;
})
);
const [moveAppId, setMoveAppId] = useState<string>();
const onMoveApp = useCallback(
async (parentId: ParentIdType) => {
if (!moveAppId) return;
await onUpdateApp(moveAppId, { parentId });
},
[moveAppId, onUpdateApp]
);
const getAppFolderList = useCallback(({ parentId }: GetResourceFolderListProps) => {
return getMyApps({
parentId,
type: AppTypeEnum.folder
}).then((res) =>
res.map((item) => ({
id: item._id,
name: item.name
}))
);
}, []);
const contextValue: AppListContextType = {
parentId,
myApps: data,
loadMyApps,
isFetchingApps,
folderDetail,
paths,
onUpdateApp,
setMoveAppId
};
return (
<AppListContext.Provider value={contextValue}>
{children}
{!!moveAppId && (
<MoveModal
moveResourceId={moveAppId}
server={getAppFolderList}
title={appT('Move app')}
onClose={() => setMoveAppId(undefined)}
onConfirm={onMoveApp}
/>
)}
</AppListContext.Provider>
);
};
export default AppListContextProvider;

View File

@@ -1,192 +1,209 @@
import React, { useCallback } from 'react';
import { Box, Grid, Flex, IconButton, Button, useDisclosure } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import { Box, Flex, Button, useDisclosure } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { delModelById } from '@/web/core/app/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { serviceSideProps } from '@/web/common/utils/i18n';
import MyIcon from '@fastgpt/web/components/common/Icon';
import PageContainer from '@/components/PageContainer';
import Avatar from '@/components/Avatar';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import CreateModal from './component/CreateModal';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import PermissionIconText from '@/components/support/permission/IconText';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useI18n } from '@/web/context/I18n';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import dynamic from 'next/dynamic';
import List from './component/List';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postCreateAppFolder } from '@/web/core/app/api/app';
import type { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditFolderModal';
import { useContextSelector } from 'use-context-selector';
import AppListContextProvider, { AppListContext } from './component/context';
import FolderPath from '@/components/common/folder/Path';
import { useRouter } from 'next/router';
import FolderSlideCard from '@/components/common/folder/SlideCard';
import { delAppById } from '@/web/core/app/api';
import {
AppDefaultPermissionVal,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import {
deleteAppCollaborators,
getCollaboratorList,
postUpdateAppCollaborators
} from '@/web/core/app/api/collaborator';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
);
const MyApps = () => {
const { t } = useTranslation();
const { toast } = useToast();
const { appT, commonT } = useI18n();
const { appT } = useI18n();
const router = useRouter();
const { isPc } = useSystemStore();
const {
paths,
parentId,
myApps,
loadMyApps,
onUpdateApp,
setMoveAppId,
isFetchingApps,
folderDetail
} = useContextSelector(AppListContext, (v) => v);
const { userInfo } = useUserStore();
const { myApps, loadMyApps } = useAppStore();
const { openConfirm, ConfirmModal } = useConfirm({
type: 'delete',
content: '确认删除该应用所有信息?'
});
const {
isOpen: isOpenCreateModal,
onOpen: onOpenCreateModal,
onClose: onCloseCreateModal
} = useDisclosure();
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
/* 点击删除 */
const onclickDelApp = useCallback(
async (id: string) => {
try {
await delModelById(id);
toast({
title: '删除成功',
status: 'success'
});
loadMyApps();
} catch (err: any) {
toast({
title: err?.message || t('common.Delete Failed'),
status: 'error'
});
}
const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, {
onSuccess() {
loadMyApps();
},
[toast, loadMyApps, t]
);
/* 加载模型 */
const { isFetching } = useQuery(['loadApps'], () => loadMyApps(), {
refetchOnMount: true
errorToast: 'Error'
});
const { runAsync: onDeleFolder } = useRequest2(delAppById, {
onSuccess() {
router.replace({
query: {
parentId: folderDetail?.parentId
}
});
},
errorToast: 'Error'
});
return (
<PageContainer isLoading={isFetching} insertProps={{ px: [5, '48px'] }}>
<Flex pt={[4, '30px']} alignItems={'center'} justifyContent={'space-between'}>
<Box letterSpacing={1} fontSize={['20px', '24px']} color={'myGray.900'}>
{appT('My Apps')}
<PageContainer
isLoading={myApps.length === 0 && isFetchingApps}
insertProps={{ px: folderDetail ? [4, 6] : [4, 10] }}
>
<Flex gap={5}>
<Box flex={'1 0 0'}>
<Flex pt={[4, 6]} alignItems={'center'} justifyContent={'space-between'}>
<FolderPath
paths={paths}
FirstPathDom={
<Box letterSpacing={1} fontSize={['md', 'lg']} color={'myGray.900'}>
{appT('My Apps')}
</Box>
}
onClick={(parentId) => {
router.push({
query: {
parentId
}
});
}}
/>
{userInfo?.team.permission.hasWritePer && (
<MyMenu
width={150}
iconSize="1.5rem"
Button={
<Button variant={'primary'} leftIcon={<AddIcon />}>
<Box>{t('common.Create New')}</Box>
</Button>
}
menuList={[
{
children: [
{
icon: 'core/app/simpleBot',
label: appT('Create bot'),
description: appT('Create one ai app'),
onClick: onOpenCreateModal
}
]
},
{
children: [
{
icon: FolderIcon,
label: t('Folder'),
onClick: () => setEditFolder({})
}
]
}
]}
/>
)}
</Flex>
<List />
</Box>
{userInfo?.team.permission.hasWritePer && (
<Button leftIcon={<AddIcon />} variant={'primaryOutline'} onClick={onOpenCreateModal}>
{commonT('New Create')}
</Button>
{!!folderDetail && isPc && (
<Box pt={[4, 6]}>
<FolderSlideCard
name={folderDetail.name}
intro={folderDetail.intro}
onEdit={() => {
setEditFolder({
id: folderDetail._id,
name: folderDetail.name,
intro: folderDetail.intro
});
}}
onMove={() => setMoveAppId(folderDetail._id)}
deleteTip={appT('Confirm delete folder tip')}
onDelete={() => onDeleFolder(folderDetail._id)}
defaultPer={{
value: folderDetail.defaultPermission,
defaultValue: AppDefaultPermissionVal,
onChange: (e) => {
return onUpdateApp(folderDetail._id, { defaultPermission: e });
}
}}
managePer={{
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: AppPermissionList,
onUpdateCollaborators: (tmbIds: string[], permission: number) => {
return postUpdateAppCollaborators({
tmbIds,
permission,
appId: folderDetail._id
});
},
onDelOneCollaborator: (tmbId: string) =>
deleteAppCollaborators({
appId: folderDetail._id,
tmbId
})
}}
/>
</Box>
)}
</Flex>
<Grid
py={[4, 6]}
gridTemplateColumns={['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']}
gridGap={5}
>
{myApps.map((app) => (
<MyTooltip
key={app._id}
label={app.permission.hasWritePer ? appT('To Settings') : appT('To Chat')}
>
<Box
lineHeight={1.5}
h={'100%'}
py={3}
px={5}
cursor={'pointer'}
borderWidth={'1.5px'}
borderColor={'borderColor.low'}
bg={'white'}
borderRadius={'md'}
userSelect={'none'}
position={'relative'}
display={'flex'}
flexDirection={'column'}
_hover={{
borderColor: 'primary.300',
boxShadow: '1.5',
'& .delete': {
display: 'flex'
},
'& .chat': {
display: 'flex'
}
}}
onClick={() => {
if (app.permission.hasWritePer) {
router.push(`/app/detail?appId=${app._id}`);
} else {
router.push(`/chat?appId=${app._id}`);
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={app.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3}>{app.name}</Box>
{app.permission.isOwner && (
<IconButton
className="delete"
position={'absolute'}
top={4}
right={4}
size={'xsSquare'}
variant={'whiteDanger'}
icon={<MyIcon name={'delete'} w={'14px'} />}
aria-label={'delete'}
display={['', 'none']}
onClick={(e) => {
e.stopPropagation();
openConfirm(() => onclickDelApp(app._id))();
}}
/>
)}
</Flex>
<Box
flex={1}
className={'textEllipsis3'}
py={2}
wordBreak={'break-all'}
fontSize={'xs'}
color={'myGray.600'}
>
{app.intro || '这个应用还没写介绍~'}
</Box>
<Flex h={'34px'} alignItems={'flex-end'}>
<Box flex={1}>
<PermissionIconText
defaultPermission={app.defaultPermission}
color={'myGray.600'}
/>
</Box>
{app.permission.hasWritePer && (
<IconButton
className="chat"
size={'xsSquare'}
variant={'whitePrimary'}
icon={
<MyTooltip label={'去聊天'}>
<MyIcon name={'core/chat/chatLight'} w={'14px'} />
</MyTooltip>
}
aria-label={'chat'}
display={['', 'none']}
onClick={(e) => {
e.stopPropagation();
router.push(`/chat?appId=${app._id}`);
}}
/>
)}
</Flex>
</Box>
</MyTooltip>
))}
</Grid>
{myApps.length === 0 && <EmptyTip text={'还没有应用,快去创建一个吧!'} pt={'30vh'} />}
<ConfirmModal />
{isOpenCreateModal && (
<CreateModal onClose={onCloseCreateModal} onSuccess={() => loadMyApps()} />
{!!editFolder && (
<EditFolderModal
{...editFolder}
onClose={() => setEditFolder(undefined)}
onCreate={(data) => onCreateFolder({ ...data, parentId })}
onEdit={({ id, ...data }) => onUpdateApp(id, data)}
/>
)}
{isOpenCreateModal && <CreateModal onClose={onCloseCreateModal} />}
</PageContainer>
);
};
function ContextRender() {
return (
<AppListContextProvider>
<MyApps />
</AppListContextProvider>
);
}
export default ContextRender;
export async function getServerSideProps(content: any) {
return {
props: {
@@ -194,5 +211,3 @@ export async function getServerSideProps(content: any) {
}
};
}
export default MyApps;

View File

@@ -108,7 +108,7 @@ const Test = ({ datasetId }: { datasetId: string }) => {
usingReRank: res.usingReRank,
limit: res.limit,
similarity: res.similarity,
usingQueryExtension: res.usingQueryExtension
queryExtensionModel: res.queryExtensionModel
};
pushDatasetTestItem(testItem);
setDatasetTestItem(testItem);
@@ -430,7 +430,7 @@ const TestResults = React.memo(function TestResults({
similarity={datasetTestItem.similarity}
limit={datasetTestItem.limit}
usingReRank={datasetTestItem.usingReRank}
usingQueryExtension={datasetTestItem.usingQueryExtension}
queryExtensionModel={datasetTestItem.queryExtensionModel}
/>
</Box>