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

@@ -9,13 +9,18 @@
"Chat Logs Tips": "Logs will record online, shared and API (chatId required) conversation records for this app",
"Chat logs": "Chat Logs",
"Confirm Del App Tip": "Confirm to delete this app and all its chat records?",
"Confirm delete folder tip": "Are you sure to delete this folder? All the following applications and corresponding chat records will be deleted, please confirm!",
"Connection is invalid": "Connection is invalid",
"Connection type is different": "Connection type is different",
"Copy Module Config": "Copy Config",
"Create bot": "App",
"Create one ai app": "Create AI app",
"Dataset Quote Template": "Knowledge Base QA Mode",
"Edit app": "Edit app",
"Export Config Successful": "Config copied, please check for important data",
"Export Configs": "Export Configs",
"Feedback Count": "User Feedback",
"Go to chat": "To chat",
"Import Configs": "Import Configs",
"Import Configs Failed": "Failed to import configs, please ensure configs are valid!",
"Input Field Settings": "Input Field Settings",
@@ -25,6 +30,7 @@
"Logs Time": "Time",
"Logs Title": "Title",
"Mark Count": "Marked Answer Count",
"Move app": "Move app",
"My Apps": "My Apps",
"Output Field Settings": "Output Field Settings",
"Paste Config": "Paste Config",

View File

@@ -49,6 +49,7 @@
"Delete Success": "Delete Success",
"Delete Tip": "Delete Tip",
"Delete Warning": "Delete Warning",
"Delete folder": "Delete",
"Detail": "Detail",
"Documents": "Documents",
"Done": "Done",
@@ -64,6 +65,8 @@
"Import failed": "Import failed",
"Import success": "Import success",
"Input": "Input",
"Input folder description": "Folder description",
"Input name": "Folder name",
"Intro": "Intro",
"Invalid Json": "Invalid JSON format, please check.",
"Last Step": "Last Step",
@@ -71,6 +74,7 @@
"Load Failed": "Load Failed",
"Loading": "Loading...",
"More settings": "More settings",
"Move": "Move",
"MultipleRowSelect": {
"No data": "No data available"
},
@@ -84,6 +88,7 @@
"OK": "OK",
"Open": "Open",
"Opened": "Opened",
"Operation": "Operation",
"Other": "Other",
"Output": "Output",
"Params": "Params",
@@ -161,7 +166,9 @@
"folder": {
"Drag Tip": "Drag me",
"Move Success": "Move successful",
"Move to": "Move to",
"No Folder": "No subdirectories, place here",
"Open folder": "Open folder",
"Root Path": "Root directory",
"empty": "This directory has nothing selectable~"
},
@@ -1212,8 +1219,12 @@
"Tools": "Tools"
},
"permission": {
"Collaborator": "",
"Default permission": "Default permission",
"Manage": "Manage",
"Not collaborator": "Not collaborator",
"Permission": "Permission",
"Permission config": "Permission config",
"Private": "Private",
"Private Tip": "Only available to oneself",
"Public": "Team",
@@ -1292,6 +1303,9 @@
"Response Quote tips": "Return quote content in the share link, but will not allow users to download the original document"
}
},
"permission": {
"Permission": "Permission"
},
"standard": {
"AI Bonus Points": "AI points",
"Expired Time": "End time",

View File

@@ -8,13 +8,18 @@
"Chat Logs Tips": "日志会记录该应用的在线、分享和 API(需填写 chatId) 对话记录",
"Chat logs": "对话日志",
"Confirm Del App Tip": "确认删除该应用及其所有聊天记录?",
"Confirm delete folder tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!",
"Connection is invalid": "连接无效",
"Connection type is different": "连接的类型不一致",
"Copy Module Config": "复制配置",
"Create bot": "应用",
"Create one ai app": "创建一个AI应用",
"Dataset Quote Template": "知识库问答模式",
"Edit app": "编辑应用",
"Export Config Successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据",
"Export Configs": "导出配置",
"Feedback Count": "用户反馈",
"Go to chat": "去对话",
"Import Configs": "导入配置",
"Import Configs Failed": "导入配置失败,请确保配置正常!",
"Input Field Settings": "输入字段编辑",
@@ -24,6 +29,7 @@
"Logs Time": "时间",
"Logs Title": "标题",
"Mark Count": "标注答案数量",
"Move app": "移动应用",
"My Apps": "我的应用",
"Output Field Settings": "输出字段编辑",
"Paste Config": "粘贴配置",

View File

@@ -49,6 +49,7 @@
"Delete Success": "删除成功",
"Delete Tip": "删除提示",
"Delete Warning": "删除警告",
"Delete folder": "删除文件夹",
"Detail": "详情",
"Documents": "文档",
"Done": "完成",
@@ -64,6 +65,8 @@
"Import failed": "导入失败",
"Import success": "导入成功",
"Input": "输入",
"Input folder description": "文件夹描述",
"Input name": "取个名字",
"Intro": "介绍",
"Invalid Json": "无效的JSON格式请注意检查。",
"Last Step": "上一步",
@@ -71,6 +74,7 @@
"Load Failed": "加载失败",
"Loading": "加载中...",
"More settings": "更多设置",
"Move": "移动",
"MultipleRowSelect": {
"No data": "没有可选值"
},
@@ -84,6 +88,7 @@
"OK": "好的",
"Open": "打开",
"Opened": "已开启",
"Operation": "操作",
"Other": "其他",
"Output": "输出",
"Params": "参数",
@@ -162,7 +167,9 @@
"folder": {
"Drag Tip": "点我可拖动",
"Move Success": "移动成功",
"Move to": "移动到",
"No Folder": "没有子目录了,就放这里吧",
"Open folder": "打开文件夹",
"Root Path": "根目录",
"empty": "这个目录已经没东西可选了~"
},
@@ -532,7 +539,6 @@
"Go Dataset": "前往知识库",
"Intro Placeholder": "这个知识库还没有介绍~",
"Manual collection": "手动数据集",
"externalFile": "外部文件库",
"My Dataset": "我的知识库",
"Name": "知识库名称",
"Query extension intro": "开启问题优化功能,可以提高提高连续对话时,知识库搜索的精度。开启该功能后,在进行知识库搜索时,会根据对话记录,利用 AI 补全问题缺失的信息。",
@@ -586,7 +592,8 @@
"success": "开始同步"
}
},
"training": {}
"training": {
}
},
"data": {
"Auxiliary Data": "辅助数据",
@@ -619,6 +626,7 @@
"unCreateCollection": "无权操作该数据",
"unLinkCollection": "不是网络链接集合"
},
"externalFile": "外部文件库",
"file": "文件",
"folder": "目录",
"import": {
@@ -1219,8 +1227,12 @@
"Tools": "工具"
},
"permission": {
"Collaborator": "协作者",
"Default permission": "默认权限",
"Manage": "管理",
"Not collaborator": "暂无协作者",
"Permission": "权限",
"Permission config": "权限配置",
"Private": "私有",
"Private Tip": "仅自己可用",
"Public": "团队",
@@ -1299,6 +1311,9 @@
"Response Quote tips": "在分享链接中返回引用内容,但不会允许用户下载原文档"
}
},
"permission": {
"Permission": "权限"
},
"standard": {
"AI Bonus Points": "AI 积分",
"Expired Time": "结束时间",

View File

@@ -1 +1,14 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1700746780241" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="67557" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M768 320v128H576V256h128L512 64 320 256h128v192H256V320L64 512l64 64 128 128V576h192v192H320l192 192 64-64 128-128H576V576h192v128l192-192-192-192z" p-id="67558" fill="#13227a"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M10.5898 1.90814C10.4269 1.74528 10.2134 1.66392 9.99998 1.66406C9.78652 1.66392 9.57302 1.74528 9.41016 1.90814C9.40148 1.91682 9.39303 1.92565 9.38481 1.93461L7.64351 3.67591C7.31807 4.00134 7.31807 4.52898 7.64351 4.85442C7.96895 5.17986 8.49659 5.17986 8.82202 4.85442L9.1672 4.50924V7.49791C9.1672 7.95815 9.5403 8.33125 10.0005 8.33125C10.4608 8.33125 10.8339 7.95815 10.8339 7.49791V4.51036L11.1779 4.85442C11.5034 5.17986 12.031 5.17986 12.3564 4.85442C12.6819 4.52898 12.6819 4.00134 12.3564 3.67591L10.6151 1.93461C10.6069 1.92565 10.5985 1.91682 10.5898 1.90814Z"
fill="#3370FF" />
<path
d="M9.1672 15.4907V12.5048C9.1672 12.0446 9.5403 11.6715 10.0005 11.6715C10.4608 11.6715 10.8339 12.0446 10.8339 12.5048V15.4896L11.1779 15.1455C11.5034 14.8201 12.031 14.8201 12.3564 15.1455C12.6819 15.471 12.6819 15.9986 12.3564 16.324L10.6151 18.0653C10.6069 18.0743 10.5985 18.0831 10.5898 18.0918C10.4269 18.2547 10.2134 18.336 9.99998 18.3359C9.78652 18.336 9.57302 18.2547 9.41016 18.0918C9.40148 18.0831 9.39303 18.0743 9.38481 18.0653L7.64351 16.324C7.31807 15.9986 7.31807 15.471 7.64351 15.1455C7.96895 14.8201 8.49659 14.8201 8.82202 15.1455L9.1672 15.4907Z"
fill="#3370FF" />
<path
d="M7.58068 9.1672C8.04092 9.1672 8.41402 9.5403 8.41402 10.0005C8.41402 10.4608 8.04092 10.8339 7.58068 10.8339H4.51036L4.85442 11.1779C5.17986 11.5034 5.17986 12.031 4.85442 12.3564C4.52898 12.6819 4.00134 12.6819 3.67591 12.3564L1.93461 10.6151C1.92565 10.6069 1.91682 10.5985 1.90814 10.5898C1.74527 10.4269 1.66391 10.2134 1.66406 9.99995C1.66393 9.7865 1.74529 9.57301 1.90814 9.41016C1.91682 9.40148 1.92565 9.39303 1.93461 9.38481L3.67591 7.64351C4.00134 7.31807 4.52898 7.31807 4.85442 7.64351C5.17986 7.96895 5.17986 8.49659 4.85442 8.82202L4.50924 9.1672H7.58068Z"
fill="#3370FF" />
<path
d="M16.3241 7.64351L18.0654 9.38481C18.0743 9.39303 18.0832 9.40148 18.0918 9.41016C18.2547 9.57302 18.3361 9.78652 18.3359 9.99998C18.3361 10.2134 18.2547 10.4269 18.0918 10.5898C18.0832 10.5985 18.0743 10.6069 18.0654 10.6151L16.3241 12.3564C15.9986 12.6819 15.471 12.6819 15.1456 12.3564C14.8201 12.031 14.8201 11.5034 15.1456 11.1779L15.4888 10.8347H12.4119C11.9517 10.8347 11.5786 10.4616 11.5786 10.0014C11.5786 9.54114 11.9517 9.16804 12.4119 9.16804H15.4916L15.1456 8.82202C14.8201 8.49659 14.8201 7.96895 15.1456 7.64351C15.471 7.31807 15.9986 7.31807 16.3241 7.64351Z"
fill="#3370FF" />
</svg>

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -39,6 +39,7 @@ export default function InputGuideBox({
);
},
{
manual: false,
refreshDeps: [text],
throttleWait: 300
}

View File

@@ -155,12 +155,13 @@ ${JSON.stringify(questionGuides)}`;
borderColor={'myGray.200'}
boxShadow={'1'}
_hover={{
bg: 'auto',
color: 'primary.600'
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'14px'} mr={2} />
<Box mr={1}>{tool.toolName}</Box>
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'1rem'} mr={2} />
<Box mr={1} fontSize={'sm'}>
{tool.toolName}
</Box>
{isChatting && !tool.response && (
<MyIcon name={'common/loading'} w={'14px'} />
)}
@@ -219,7 +220,14 @@ ${toolResponse}`}
<ChatAvatar src={avatar} type={type} />
{!!chatStatusMap && statusBoxData && isLastChild && (
<Flex alignItems={'center'} px={3} py={'1.5px'} borderRadius="md" bg={chatStatusMap.bg}>
<Flex
alignItems={'center'}
px={3}
py={'1.5px'}
borderRadius="md"
bg={chatStatusMap.bg}
fontSize={'sm'}
>
<Box
className={styles.statusAnimation}
bg={chatStatusMap.color}

View File

@@ -0,0 +1,116 @@
import React, { useCallback } from 'react';
import { ModalFooter, ModalBody, Input, Button, Box, Textarea, HStack } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal/index';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { getErrText } from '@fastgpt/global/common/error/utils';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@/components/Avatar';
import { useToast } from '@fastgpt/web/hooks/useToast';
export type EditResourceInfoFormType = {
id: string;
name: string;
avatar?: string;
intro?: string;
};
const EditResourceModal = ({
onClose,
onEdit,
title,
...defaultForm
}: EditResourceInfoFormType & {
title: string;
onClose: () => void;
onEdit: (data: EditResourceInfoFormType) => any;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const { register, watch, setValue, handleSubmit } = useForm<EditResourceInfoFormType>({
defaultValues: defaultForm
});
const avatar = watch('avatar');
const { runAsync: onSave, loading } = useRequest2(
(data: EditResourceInfoFormType) => onEdit(data),
{
onSuccess: (res) => {
onClose();
}
}
);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.appAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
} catch (err: any) {
toast({
title: getErrText(err, t('common.error.Select avatar failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
return (
<MyModal isOpen onClose={onClose} iconSrc={avatar} title={title}>
<ModalBody>
<Box>
<FormLabel mb={1}>{t('core.app.Name and avatar')}</FormLabel>
<HStack spacing={4}>
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={avatar}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
</MyTooltip>
<Input
{...register('name', { required: true })}
bg={'myGray.50'}
autoFocus
maxLength={20}
/>
</HStack>
</Box>
<Box mt={4}>
<FormLabel mb={1}>{t('common.Intro')}</FormLabel>
<Textarea {...register('intro')} bg={'myGray.50'} maxLength={200} />
</Box>
</ModalBody>
<ModalFooter>
<Button isLoading={loading} onClick={handleSubmit(onSave)}>
{t('common.Confirm')}
</Button>
</ModalFooter>
<File onSelect={onSelectFile} />
</MyModal>
);
};
export default EditResourceModal;

View File

@@ -1,4 +1,3 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Flex } from '@chakra-ui/react';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import React, { useMemo } from 'react';
@@ -37,16 +36,19 @@ const ParentPaths = (props: {
{concatPaths.map((item, i) => (
<Flex key={item.parentId || i} alignItems={'center'}>
<Box
fontSize={['sm', fontSize || 'md']}
py={1}
px={[1, 2]}
fontSize={['sm', fontSize || 'sm']}
py={0.5}
px={1.5}
borderRadius={'md'}
{...(i === concatPaths.length - 1
? {
cursor: 'default'
cursor: 'default',
color: 'myGray.700',
fontWeight: 'bold'
}
: {
cursor: 'pointer',
color: 'myGray.600',
_hover: {
bg: 'myGray.100'
},
@@ -58,7 +60,9 @@ const ParentPaths = (props: {
{item.parentName}
</Box>
{i !== concatPaths.length - 1 && (
<MyIcon name={'common/rightArrowLight'} color={'myGray.500'} w={'14px'} />
<Box mx={1.5} color={'myGray.500'}>
/
</Box>
)}
</Flex>
))}

View File

@@ -0,0 +1,184 @@
import React, { useCallback, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react';
import {
GetResourceFolderListProps,
GetResourceFolderListItemResponse,
ParentIdType
} from '@fastgpt/global/common/parentFolder/type';
import { useMemoizedFn, useMount } from 'ahooks';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
type FolderItemType = {
id: string;
name: string;
open: boolean;
children?: FolderItemType[];
};
const rootId = 'root';
type Props = {
moveResourceId: string;
title: string;
server: (e: GetResourceFolderListProps) => Promise<GetResourceFolderListItemResponse[]>;
onConfirm: (id: ParentIdType) => Promise<any>;
onClose: () => void;
};
const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props) => {
const { t } = useTranslation();
const [selectedId, setSelectedId] = React.useState<string>();
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
const [folderList, setFolderList] = useState<FolderItemType[]>([]);
const { runAsync: requestServer } = useRequest2((e: GetResourceFolderListProps) => {
if (requestingIdList.includes(e.parentId)) return Promise.reject(null);
setRequestingIdList((state) => [...state, e.parentId]);
return server(e).finally(() =>
setRequestingIdList((state) => state.filter((id) => id !== e.parentId))
);
}, {});
useMount(async () => {
const data = await requestServer({ parentId: null });
setFolderList([
{
id: rootId,
name: t('common.folder.Root Path'),
open: true,
children: data.map((item) => ({
id: item.id,
name: item.name,
open: false
}))
}
]);
});
const RenderList = useMemoizedFn(
({ list, index = 0 }: { list: FolderItemType[]; index?: number }) => {
return (
<>
{list
// can not move to itself
.filter((item) => moveResourceId !== item.id)
.map((item) => (
<Box key={item.id} _notLast={{ mb: 0.5 }} userSelect={'none'}>
<Flex
alignItems={'center'}
cursor={'pointer'}
py={1}
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
pr={2}
borderRadius={'md'}
_hover={{
bg: 'myGray.100'
}}
{...(item.id === selectedId
? {
bg: 'primary.50 !important',
onClick: () => setSelectedId(undefined)
}
: {
onClick: () => setSelectedId(item.id)
})}
>
{index !== 0 && (
<Flex
alignItems={'center'}
justifyContent={'center'}
visibility={!item.children || item.children.length > 0 ? 'visible' : 'hidden'}
w={'1.25rem'}
h={'1.25rem'}
cursor={'pointer'}
borderRadius={'xs'}
_hover={{
bg: 'rgba(31, 35, 41, 0.08)'
}}
onClick={async (e) => {
e.stopPropagation();
if (requestingIdList.includes(item.id)) return;
if (!item.children) {
const data = await requestServer({ parentId: item.id });
item.children = data.map((item) => ({
id: item.id,
name: item.name,
open: false
}));
}
item.open = !item.open;
setFolderList([...folderList]);
}}
>
<MyIcon
name={
requestingIdList.includes(item.id)
? 'common/loading'
: 'common/rightArrowFill'
}
w={'1.25rem'}
color={'myGray.500'}
transform={item.open ? 'rotate(90deg)' : 'none'}
/>
</Flex>
)}
<MyIcon ml={index !== 0 ? '0.5rem' : 0} name={FolderIcon} w={'1.25rem'} />
<Box fontSize={'sm'} ml={2}>
{item.name}
</Box>
</Flex>
{item.children && item.open && (
<Box mt={0.5}>
<RenderList list={item.children} index={index + 1} />
</Box>
)}
</Box>
))}
</>
);
}
);
const { runAsync: onConfirmSelect, loading: confirming } = useRequest2(
() => {
if (selectedId) {
return onConfirm(selectedId === rootId ? null : selectedId);
}
return Promise.reject('');
},
{
onSuccess: () => {
onClose();
},
successToast: t('common.folder.Move Success')
}
);
return (
<MyModal
isLoading={folderList.length === 0}
iconSrc="/imgs/modal/move.svg"
isOpen
w={'30rem'}
title={title}
onClose={onClose}
>
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'}>
<RenderList list={folderList} />
</ModalBody>
<ModalFooter>
<Button isLoading={confirming} isDisabled={!selectedId} onClick={onConfirmSelect}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default MoveModal;

View File

@@ -0,0 +1,68 @@
import { Box, Flex } from '@chakra-ui/react';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import React, { useMemo } from 'react';
import { useTranslation } from 'next-i18next';
const FolderPath = (props: {
paths: ParentTreePathItemType[];
rootName?: string;
FirstPathDom?: React.ReactNode;
onClick: (parentId: string) => void;
fontSize?: string;
}) => {
const { t } = useTranslation();
const { paths, rootName = t('common.folder.Root Path'), FirstPathDom, onClick, fontSize } = props;
const concatPaths = useMemo(
() => [
{
parentId: '',
parentName: rootName
},
...paths
],
[rootName, paths]
);
return paths.length === 0 && !!FirstPathDom ? (
<>{FirstPathDom}</>
) : (
<Flex flex={1} ml={-1.5}>
{concatPaths.map((item, i) => (
<Flex key={item.parentId || i} alignItems={'center'}>
<Box
fontSize={['sm', fontSize || 'sm']}
py={0.5}
px={1.5}
borderRadius={'md'}
{...(i === concatPaths.length - 1
? {
cursor: 'default',
color: 'myGray.700',
fontWeight: 'bold'
}
: {
cursor: 'pointer',
color: 'myGray.600',
_hover: {
bg: 'myGray.100'
},
onClick: () => {
onClick(item.parentId);
}
})}
>
{item.parentName}
</Box>
{i !== concatPaths.length - 1 && (
<Box mx={1.5} color={'myGray.500'}>
/
</Box>
)}
</Flex>
))}
</Flex>
);
};
export default React.memo(FolderPath);

View File

@@ -0,0 +1,140 @@
import React, { useState } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import {
GetResourceFolderListProps,
GetResourceListItemResponse,
ParentIdType
} from '@fastgpt/global/common/parentFolder/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Loading from '@fastgpt/web/components/common/MyLoading';
import Avatar from '@/components/Avatar';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useMemoizedFn } from 'ahooks';
type ResourceItemType = GetResourceListItemResponse & {
open: boolean;
children?: ResourceItemType[];
};
const SelectOneResource = ({
server,
value,
onSelect
}: {
server: (e: GetResourceFolderListProps) => Promise<GetResourceListItemResponse[]>;
value?: ParentIdType;
onSelect: (e?: string) => any;
}) => {
const [dataList, setDataList] = useState<ResourceItemType[]>([]);
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
const { runAsync: requestServer } = useRequest2((e: GetResourceFolderListProps) => {
if (requestingIdList.includes(e.parentId)) return Promise.reject(null);
setRequestingIdList((state) => [...state, e.parentId]);
return server(e).finally(() =>
setRequestingIdList((state) => state.filter((id) => id !== e.parentId))
);
}, {});
const { loading } = useRequest2(() => requestServer({ parentId: null }), {
manual: false,
onSuccess: (data) => {
setDataList(
data.map((item) => ({
...item,
open: false
}))
);
}
});
const Render = useMemoizedFn(
({ list, index = 0 }: { list: ResourceItemType[]; index?: number }) => {
return (
<>
{list.map((item) => (
<Box key={item.id} _notLast={{ mb: 0.5 }} userSelect={'none'}>
<Flex
alignItems={'center'}
cursor={'pointer'}
py={1}
pl={`${1.25 * index + 0.5}rem`}
pr={2}
borderRadius={'md'}
_hover={{
bg: 'myGray.100'
}}
{...(item.id === value
? {
bg: 'primary.50 !important',
onClick: () => onSelect(undefined)
}
: {
onClick: async () => {
// folder => open(request children) or close
if (item.isFolder) {
if (!item.children) {
const data = await requestServer({ parentId: item.id });
item.children = data.map((item) => ({
...item,
open: false
}));
}
item.open = !item.open;
setDataList([...dataList]);
} else {
onSelect(item.id);
}
}
})}
>
<Flex
alignItems={'center'}
justifyContent={'center'}
visibility={
item.isFolder && (!item.children || item.children.length > 0)
? 'visible'
: 'hidden'
}
w={'1.25rem'}
h={'1.25rem'}
cursor={'pointer'}
borderRadius={'xs'}
_hover={{
bg: 'rgba(31, 35, 41, 0.08)'
}}
>
<MyIcon
name={
requestingIdList.includes(item.id)
? 'common/loading'
: 'common/rightArrowFill'
}
w={'14px'}
color={'myGray.500'}
transform={item.open ? 'rotate(90deg)' : 'none'}
/>
</Flex>
<Avatar ml={index !== 0 ? '0.5rem' : 0} src={item.avatar} w={'1.25rem'} />
<Box fontSize={'sm'} ml={2}>
{item.name}
</Box>
</Flex>
{item.children && item.open && (
<Box mt={0.5}>
<Render list={item.children} index={index + 1} />
</Box>
)}
</Box>
))}
</>
);
}
);
return loading ? <Loading fixed={false} /> : <Render list={dataList} />;
};
export default SelectOneResource;

View File

@@ -0,0 +1,178 @@
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyDivider from '@fastgpt/web/components/common/MyDivider';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import {
CollaboratorContextProvider,
MemberManagerInputPropsType
} from '../../support/permission/MemberManager/context';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const FolderSlideCard = ({
name,
intro,
onEdit,
onMove,
deleteTip,
onDelete,
defaultPer,
managePer
}: {
name: string;
intro?: string;
onEdit: () => void;
onMove: () => void;
deleteTip: string;
onDelete: () => void;
defaultPer: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
}) => {
const { t } = useTranslation();
const { ConfirmModal, openConfirm } = useConfirm({
type: 'delete',
content: deleteTip
});
return (
<Box w={'13rem'}>
<Box>
<HStack>
<MyIcon name={FolderIcon} w={'1.5rem'} />
<Box color={'myGray.900'}>{name}</Box>
<MyIcon
name={'edit'}
_hover={{ color: 'primary.600' }}
w={'0.875rem'}
cursor={'pointer'}
onClick={onEdit}
/>
</HStack>
<Box mt={3} fontSize={'sm'} color={'myGray.500'} cursor={'pointer'} onClick={onEdit}>
{intro || '暂无介绍'}
</Box>
</Box>
{managePer.permission.hasManagePer && (
<>
<MyDivider my={6} />
<Box>
<FormLabel>{t('common.Operation')}</FormLabel>
<Button
variant={'transparentBase'}
pl={1}
leftIcon={<MyIcon name={'common/file/move'} w={'1rem'} />}
transform={'none !important'}
w={'100%'}
justifyContent={'flex-start'}
size={'sm'}
fontSize={'mini'}
mt={4}
onClick={onMove}
>
{t('common.Move')}
</Button>
<Button
variant={'transparentDanger'}
pl={1}
leftIcon={<MyIcon name={'delete'} w={'1rem'} />}
transform={'none !important'}
w={'100%'}
justifyContent={'flex-start'}
size={'sm'}
fontSize={'mini'}
mt={3}
onClick={() => {
openConfirm(onDelete)();
}}
>
{t('common.Delete folder')}
</Button>
</Box>
</>
)}
<MyDivider my={6} />
<Box>
<FormLabel>{t('support.permission.Permission')}</FormLabel>
{managePer.permission.hasManagePer && (
<Box mt={5}>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('permission.Default permission')}
</Box>
<DefaultPermissionList
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
onChange={defaultPer.onChange}
/>
</Box>
)}
<Box mt={6}>
<CollaboratorContextProvider {...managePer}>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>
<Flex alignItems="center" justifyContent="space-between">
<Box fontSize={'sm'} color={'myGray.500'}>
{t('permission.Collaborator')}
</Box>
{managePer.permission.hasManagePer && (
<HStack spacing={3}>
<MyTooltip label={t('permission.Manage')}>
<MyIcon
w="1rem"
name="common/settingLight"
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={onOpenManageModal}
/>
</MyTooltip>
<MyTooltip label={t('common.Add')}>
<MyIcon
w="1rem"
name="support/permission/collaborator"
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={onOpenAddMember}
/>
</MyTooltip>
</HStack>
)}
</Flex>
<MemberListCard
mt={2}
tagStyle={{
type: 'borderSolid',
colorSchema: 'gray'
}}
/>
</>
);
}}
</CollaboratorContextProvider>
</Box>
</Box>
<ConfirmModal />
</Box>
);
};
export default FolderSlideCard;

View File

@@ -0,0 +1,54 @@
import React, { useState, DragEvent, useCallback } from 'react';
import type { BoxProps } from '@chakra-ui/react';
export const useFolderDrag = ({
onDrop,
activeStyles
}: {
onDrop: (dragId: string, targetId: string) => any;
activeStyles: BoxProps;
}) => {
const [dragId, setDragId] = useState<string>();
const [targetId, setTargetId] = useState<string>();
const getBoxProps = useCallback(
({ dataId, isFolder }: { dataId: string; isFolder: boolean }) => {
return {
draggable: true,
'data-drag-id': isFolder ? dataId : undefined,
onDragStart: (e: DragEvent<HTMLDivElement>) => {
setDragId(dataId);
},
onDragOver: (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
const targetId = e.currentTarget.getAttribute('data-drag-id');
if (!targetId) return;
setTargetId(targetId);
},
onDragLeave: (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setTargetId(undefined);
},
onDrop: (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
if (targetId && dragId && targetId !== dragId) {
onDrop(dragId, targetId);
}
setTargetId(undefined);
setDragId(undefined);
},
...(activeStyles &&
targetId === dataId && {
...activeStyles
})
};
},
[activeStyles, dragId, onDrop, targetId]
);
return {
getBoxProps
};
};

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
@@ -68,6 +68,14 @@ const DatasetParamsModal = ({
const [refresh, setRefresh] = useState(false);
const [currentTabType, setCurrentTabType] = useState(SearchSettingTabEnum.searchMode);
const chatModelSelectList = (() =>
llmModelList
.filter((model) => model.usedInQueryExtension)
.map((item) => ({
value: item.model,
label: item.name
})))();
const { register, setValue, getValues, handleSubmit, watch } = useForm<DatasetParamsProps>({
defaultValues: {
limit,
@@ -75,7 +83,7 @@ const DatasetParamsModal = ({
searchMode,
usingReRank: !!usingReRank && teamPlanStatus?.standardConstants?.permissionReRank !== false,
datasetSearchUsingExtensionQuery,
datasetSearchExtensionModel: datasetSearchExtensionModel ?? llmModelList[0]?.model,
datasetSearchExtensionModel: datasetSearchExtensionModel || chatModelSelectList[0]?.value,
datasetSearchExtensionBg
}
});
@@ -85,14 +93,6 @@ const DatasetParamsModal = ({
const usingReRankWatch = watch('usingReRank');
const searchModeWatch = watch('searchMode');
const chatModelSelectList = (() =>
llmModelList
.filter((model) => model.usedInQueryExtension)
.map((item) => ({
value: item.model,
label: item.name
})))();
const searchModeList = useMemo(() => {
const list = Object.values(DatasetSearchModeMap);
return list;
@@ -109,6 +109,15 @@ const DatasetParamsModal = ({
return usingReRank !== undefined && reRankModelList.length > 0;
}, [reRankModelList.length, usingReRank]);
useEffect(() => {
if (datasetSearchUsingCfrForm) {
!queryExtensionModel &&
setValue('datasetSearchExtensionModel', chatModelSelectList[0]?.value);
} else {
setValue('datasetSearchExtensionModel', '');
}
}, [chatModelSelectList, datasetSearchUsingCfrForm, queryExtensionModel, setValue]);
return (
<MyModal
isOpen={true}
@@ -270,7 +279,7 @@ const DatasetParamsModal = ({
{t('core.dataset.Query extension intro')}
</Box>
<Flex mt={3} alignItems={'center'}>
<Box flex={'1 0 0'}>{t('core.dataset.search.Using query extension')}</Box>
<FormLabel flex={'1 0 0'}>{t('core.dataset.search.Using query extension')}</FormLabel>
<Switch {...register('datasetSearchUsingExtensionQuery')} />
</Flex>
{datasetSearchUsingCfrForm === true && (

View File

@@ -237,7 +237,6 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () =>
});
},
{
manual: true,
onSuccess() {
setNewData(undefined);
},

View File

@@ -14,22 +14,31 @@ const SearchParamsTip = ({
limit = 1500,
responseEmptyText,
usingReRank = false,
usingQueryExtension = false
queryExtensionModel
}: {
searchMode: `${DatasetSearchModeEnum}`;
similarity?: number;
limit?: number;
responseEmptyText?: string;
usingReRank?: boolean;
usingQueryExtension?: boolean;
queryExtensionModel?: string;
}) => {
const { t } = useTranslation();
const { reRankModelList } = useSystemStore();
const { reRankModelList, llmModelList } = useSystemStore();
const hasReRankModel = reRankModelList.length > 0;
const hasEmptyResponseMode = responseEmptyText !== undefined;
const hasSimilarityMode = usingReRank || searchMode === DatasetSearchModeEnum.embedding;
const extensionModelName = useMemo(
() =>
queryExtensionModel
? llmModelList.find((item) => item.model === queryExtensionModel)?.name ??
llmModelList[0]?.name
: undefined,
[llmModelList, queryExtensionModel]
);
return (
<TableContainer
bg={'primary.50'}
@@ -73,8 +82,8 @@ const SearchParamsTip = ({
{usingReRank ? '✅' : '❌'}
</Td>
)}
<Td pt={0} pb={2}>
{usingQueryExtension ? '✅' : '❌'}
<Td pt={0} pb={2} fontSize={'mini'}>
{extensionModelName ? extensionModelName : '❌'}
</Td>
{hasEmptyResponseMode && <Th>{responseEmptyText !== '' ? '✅' : '❌'}</Th>}
</Tr>

View File

@@ -1,108 +1,78 @@
import React, { useMemo } from 'react';
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useQuery } from '@tanstack/react-query';
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'next-i18next';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import SelectOneResource from '@/components/common/folder/SelectOneResource';
import {
GetResourceFolderListProps,
GetResourceListItemResponse
} from '@fastgpt/global/common/parentFolder/type';
import { getMyApps } from '@/web/core/app/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const SelectAppModal = ({
defaultApps = [],
value,
filterAppIds = [],
max = 1,
onClose,
onSuccess
}: {
defaultApps: string[];
value?: SelectAppItemType;
filterAppIds?: string[];
max?: number;
onClose: () => void;
onSuccess: (e: SelectAppItemType[]) => void;
onSuccess: (e: SelectAppItemType) => void;
}) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const theme = useTheme();
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
/* 加载模型 */
const { myApps, loadMyApps } = useAppStore();
const { isLoading } = useQuery(['loadMyApos'], () => loadMyApps());
const [selectedApp, setSelectedApp] = useState<SelectAppItemType | undefined>(value);
const apps = useMemo(
() => myApps.filter((app) => !filterAppIds.includes(app._id)),
[myApps, filterAppIds]
const getAppList = useCallback(
async ({ parentId }: GetResourceFolderListProps) => {
return getMyApps({ parentId }).then((res) =>
res
.filter((item) => !filterAppIds.includes(item._id))
.map<GetResourceListItemResponse>((item) => ({
id: item._id,
name: item.name,
avatar: item.avatar,
isFolder: item.type === AppTypeEnum.folder
}))
);
},
[filterAppIds]
);
return (
<MyModal
isOpen
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
title={`选择应用`}
iconSrc="/imgs/workflow/ai.svg"
onClose={onClose}
position={'relative'}
w={'600px'}
>
<ModalBody
display={'grid'}
gridTemplateColumns={['1fr', 'repeat(3, minmax(0, 1fr))']}
gridGap={4}
>
{apps.map((app) => (
<Flex
key={app._id}
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
p={2}
cursor={'pointer'}
{...(selectedApps.includes(app._id)
? {
bg: 'primary.100',
onClick: () => {
setSelectedApps(selectedApps.filter((e) => e !== app._id));
}
}
: {
onClick: () => {
if (max === 1) {
setSelectedApps([app._id]);
} else if (selectedApps.length < max) {
setSelectedApps([...selectedApps, app._id]);
}
}
})}
>
<Avatar src={app.avatar} w={['16px', '22px']} />
<Box fontSize={'sm'} color={'myGray.900'} ml={1}>
{app.name}
</Box>
</Flex>
))}
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'} position={'relative'}>
<SelectOneResource
value={selectedApp?.id}
onSelect={(id) => setSelectedApp(id ? { id } : undefined)}
server={getAppList}
/>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common.Close')}
{t('common.Cancel')}
</Button>
<Button
ml={2}
isDisabled={!selectedApp}
onClick={() => {
onSuccess(
apps
.filter((app) => selectedApps.includes(app._id))
.map((app) => ({
id: app._id,
name: app.name,
logo: app.avatar
}))
);
if (!selectedApp) return;
onSuccess(selectedApp);
onClose();
}}
>
{t('common.Confirm')}
</Button>
</ModalFooter>
<Loading loading={isLoading} fixed={false} />
</MyModal>
);
};

View File

@@ -1,16 +1,17 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex, useDisclosure, useTheme } from '@chakra-ui/react';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import SelectAppModal from '../../../../SelectAppModal';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppDetailById } from '@/web/core/app/api';
const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const theme = useTheme();
const filterAppIds = useContextSelector(WorkflowContext, (ctx) => ctx.filterAppIds);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
@@ -21,8 +22,28 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
} = useDisclosure();
const value = item.value as SelectAppItemType | undefined;
const filterAppString = useMemo(() => filterAppIds?.join(',') || '', [filterAppIds]);
const { data: appDetail, loading } = useRequest2(
() => {
if (value?.id) return getAppDetailById(value.id);
return Promise.resolve(null);
},
{
manual: false,
refreshDeps: [value?.id],
errorToast: 'Error',
onError() {
onChangeNode({
nodeId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: undefined
}
});
}
}
);
const Render = useMemo(() => {
return (
@@ -33,26 +54,22 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
{t('core.module.Select app')}
</Button>
) : (
<Flex
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
bg={'white'}
px={3}
py={2}
<Button
isLoading={loading}
w={'100%'}
justifyContent={loading ? 'center' : 'flex-start'}
variant={'whiteFlow'}
leftIcon={<Avatar src={appDetail?.avatar} w={6} />}
>
<Avatar src={value?.logo} w={6} />
<Box fontWeight={'medium'} ml={2}>
{value?.name}
</Box>
</Flex>
{appDetail?.name}
</Button>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
defaultApps={item.value?.id ? [item.value.id] : []}
filterAppIds={filterAppString.split(',')}
value={item.value}
filterAppIds={filterAppIds}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
@@ -61,7 +78,7 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
key: 'app',
value: {
...item,
value: e[0]
value: e
}
});
}}
@@ -70,15 +87,17 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
</>
);
}, [
filterAppString,
appDetail?.avatar,
appDetail?.name,
filterAppIds,
isOpenSelectApp,
item,
loading,
nodeId,
onChangeNode,
onCloseSelectApp,
onOpenSelectApp,
t,
theme.borders.base,
value
]);

View File

@@ -82,7 +82,7 @@ const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
similarity={data.similarity}
limit={data.limit}
usingReRank={data.usingReRank}
usingQueryExtension={data.datasetSearchUsingExtensionQuery}
queryExtensionModel={data.datasetSearchExtensionModel}
/>
</>
);

View File

@@ -0,0 +1,96 @@
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { MemberManagerInputPropsType, CollaboratorContextProvider } from '../MemberManager/context';
import { Box, Button, Flex, HStack, ModalBody } from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import DefaultPermissionList from '../DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
export type ConfigPerModalProps = {
avatar?: string;
name: string;
defaultPer: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
};
const ConfigPerModal = ({
avatar,
name,
defaultPer,
managePer,
onClose
}: ConfigPerModalProps & {
onClose: () => void;
}) => {
const { t } = useTranslation();
return (
<MyModal
isOpen
iconSrc="/imgs/modal/key.svg"
onClose={onClose}
title={t('permission.Permission config')}
>
<ModalBody>
<HStack>
<Avatar src={avatar} w={'1.75rem'} />
<Box fontSize={'lg'}>{name}</Box>
</HStack>
<Box mt={6}>
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
<DefaultPermissionList
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
onChange={defaultPer.onChange}
/>
</Box>
<Box mt={4}>
<CollaboratorContextProvider {...managePer}>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>
<Flex
alignItems="center"
flexDirection="row"
justifyContent="space-between"
w="full"
>
<Box fontSize={'sm'}>{t('permission.Collaborator')}</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>
</ModalBody>
</MyModal>
);
};
export default ConfigPerModal;

View File

@@ -3,6 +3,8 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
import { useTranslation } from 'next-i18next';
import React from 'react';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
export enum defaultPermissionEnum {
private = 'private',
@@ -13,16 +15,16 @@ export enum defaultPermissionEnum {
type Props = Omit<BoxProps, 'onChange'> & {
per: PermissionValueType;
defaultPer: PermissionValueType;
readPer: PermissionValueType;
writePer: PermissionValueType;
onChange: (v: PermissionValueType) => void;
readPer?: PermissionValueType;
writePer?: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any> | any;
};
const DefaultPermissionList = ({
per,
defaultPer,
readPer,
writePer,
readPer = ReadPermissionVal,
writePer = WritePermissionVal,
onChange,
...styles
}: Props) => {
@@ -33,14 +35,17 @@ const DefaultPermissionList = ({
{ label: '团队可编辑', value: writePer }
];
const { runAsync: onRequestChange, loading } = useRequest2(async (v: PermissionValueType) =>
onChange(v)
);
return (
<Box {...styles}>
<MySelect
isLoading={loading}
list={defaultPermissionSelectList}
value={per}
onchange={(v) => {
onChange(v);
}}
onchange={onRequestChange}
/>
</Box>
);

View File

@@ -23,7 +23,6 @@ import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getTeamMembers } from '@/web/support/user/team/api';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { ChevronDownIcon } from '@chakra-ui/icons';
import Avatar from '@/components/Avatar';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
@@ -32,11 +31,10 @@ export type AddModalPropsType = {
onClose: () => void;
};
export function AddMemberModal({ onClose }: AddModalPropsType) {
const toast = useToast();
function AddMemberModal({ onClose }: AddModalPropsType) {
const { userInfo } = useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPreLabelList } =
const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList } =
useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>('');
const {
@@ -50,7 +48,7 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
});
const filterMembers = useMemo(() => {
return members.filter((item) => {
if (item.permission.isOwner) return false;
// if (item.permission.isOwner) return false;
if (item.tmbId === userInfo?.team?.tmbId) return false;
if (!searchText) return true;
return item.memberName.includes(searchText);
@@ -60,8 +58,8 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => {
return getPreLabelList(selectedPermission).join('、');
}, [getPreLabelList, selectedPermission]);
return getPerLabelList(selectedPermission).join('、');
}, [getPerLabelList, selectedPermission]);
const { mutate: onConfirm, isLoading: isUpdating } = useRequest({
mutationFn: () => {
@@ -85,6 +83,7 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="55% 45%"
fontSize={'sm'}
>
<Flex
flexDirection="column"
@@ -141,7 +140,9 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
<MyAvatar src={member.avatar} w="32px" />
<Box ml="2">{member.memberName}</Box>
</Flex>
{!!collaborator && <PermissionTags permission={collaborator.permission} />}
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</Flex>
</Flex>
);
@@ -210,3 +211,5 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
</MyModal>
);
}
export default AddMemberModal;

View File

@@ -18,10 +18,11 @@ import PermissionTags from './PermissionTags';
import Avatar from '@/components/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Loading from '@fastgpt/web/components/common/MyLoading';
export type ManageModalProps = {
onClose: () => void;
@@ -29,14 +30,12 @@ export type ManageModalProps = {
function ManageModal({ onClose }: ManageModalProps) {
const { userInfo } = useUserStore();
const { collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } =
useContextSelector(CollaboratorContext, (v) => v);
const { mutate: onDelete, isLoading: isDeleting } = useRequest({
mutationFn: (tmbId: string) => onDelOneCollaborator(tmbId)
});
const { runAsync: onDelete, loading: isDeleting } = useRequest2((tmbId: string) =>
onDelOneCollaborator(tmbId)
);
const { mutate: onUpdate, isLoading: isUpdating } = useRequest({
mutationFn: ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => {
@@ -49,14 +48,7 @@ function ManageModal({ onClose }: ManageModalProps) {
const loading = isDeleting || isUpdating;
return (
<MyModal
isLoading={loading}
isOpen
onClose={onClose}
minW="600px"
title="管理协作者"
iconSrc="common/settingLight"
>
<MyModal isOpen onClose={onClose} minW="600px" title="管理协作者" iconSrc="common/settingLight">
<ModalBody>
<TableContainer borderRadius="md" minH="400px">
<Table>
@@ -86,26 +78,28 @@ function ManageModal({ onClose }: ManageModalProps) {
</Flex>
</Td>
<Td border="none">
<PermissionTags permission={item.permission} />
<PermissionTags permission={item.permission.value} />
</Td>
<Td border="none">
{item.tmbId !== userInfo?.team?.tmbId && (
<PermissionSelect
Button={
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
}
value={item.permission}
onChange={(per) => {
onUpdate({
tmbId: item.tmbId,
per
});
}}
onDelete={() => {
onDelete(item.tmbId);
}}
/>
)}
{/* Not self; Not owner and other manager */}
{item.tmbId !== userInfo?.team?.tmbId &&
(permission.isOwner || !item.permission.hasManagePer) && (
<PermissionSelect
Button={
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
}
value={item.permission.value}
onChange={(per) => {
onUpdate({
tmbId: item.tmbId,
per
});
}}
onDelete={() => {
onDelete(item.tmbId);
}}
/>
)}
</Td>
</Tr>
);
@@ -114,6 +108,7 @@ function ManageModal({ onClose }: ManageModalProps) {
</Table>
{collaboratorList?.length === 0 && <EmptyTip text={'暂无协作者'} />}
</TableContainer>
{loading && <Loading fixed={false} />}
</ModalBody>
</MyModal>
);

View File

@@ -0,0 +1,42 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import MyBox from '@fastgpt/web/components/common/MyBox';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
import Tag, { TagProps } from '@fastgpt/web/components/common/Tag';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'next-i18next';
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
const { t } = useTranslation();
const { collaboratorList, isFetchingCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
return (
<MyBox isLoading={isFetchingCollaborator} userSelect={'none'} {...props}>
{collaboratorList?.length === 0 ? (
<Box p={3} color="myGray.600" fontSize={'xs'} textAlign={'center'}>
{t('permission.Not collaborator')}
</Box>
) : (
<Flex gap="2" flexWrap={'wrap'}>
{collaboratorList?.map((member) => {
return (
<Tag key={member.tmbId} type={'fill'} colorSchema="white" {...tagStyle}>
<Avatar src={member.avatar} w="1.25rem" />
<Box fontSize={'sm'}>{member.name}</Box>
</Tag>
);
})}
</Flex>
)}
</MyBox>
);
};
export default MemberListCard;

View File

@@ -49,7 +49,7 @@ function PermissionSelect({
...props
}: PermissionSelectProps) {
const { t } = useTranslation();
const { permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const ref = useRef<HTMLDivElement>(null);
const closeTimer = useRef<any>();
@@ -66,10 +66,16 @@ function PermissionSelect({
});
return {
singleCheckBoxList: list.filter((item) => item.checkBoxType === 'single'),
singleCheckBoxList: list
.filter((item) => item.checkBoxType === 'single')
.filter((item) => {
if (permission.isOwner) return true;
if (item.value === permissionList['manage'].value) return false;
return true;
}),
multipleCheckBoxList: list.filter((item) => item.checkBoxType === 'multiple')
};
}, [permissionList]);
}, [permission.isOwner, permissionList]);
const selectedSingleValue = useMemo(() => {
const per = new Permission({ per: value });
@@ -88,6 +94,12 @@ function PermissionSelect({
.map((item) => item.value);
}, [permissionSelectList.multipleCheckBoxList, value]);
const onSelectPer = (per: PermissionValueType) => {
if (per === value) return;
onChange(per);
setIsOpen(false);
};
useOutsideClick({
ref: ref,
handler: () => {
@@ -151,8 +163,7 @@ function PermissionSelect({
const per = new Permission({ per: value });
per.removePer(selectedSingleValue);
per.addPer(item.value);
onChange(per.value);
setIsOpen(false);
onSelectPer(per.value);
};
return (

View File

@@ -10,9 +10,9 @@ export type PermissionTagsProp = {
};
function PermissionTags({ permission }: PermissionTagsProp) {
const { getPreLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const { getPerLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const perTagList = getPreLabelList(permission);
const perTagList = getPerLabelList(permission);
return (
<Flex gap="2" alignItems="center">

View File

@@ -1,3 +1,4 @@
import { BoxProps, useDisclosure } from '@chakra-ui/react';
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import { PermissionList } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
@@ -5,8 +6,14 @@ import { PermissionListType, PermissionValueType } from '@fastgpt/global/support
import { useQuery } from '@tanstack/react-query';
import { ReactNode, useCallback } from 'react';
import { createContext } from 'use-context-selector';
import dynamic from 'next/dynamic';
import MemberListCard, { MemberListCardProps } from './MemberListCard';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
export type MemberManagerInputPropsType = {
permission: Permission;
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
onUpdateCollaborators: (tmbIds: string[], permission: PermissionValueType) => any;
@@ -16,7 +23,12 @@ export type MemberManagerPropsType = MemberManagerInputPropsType & {
collaboratorList: CollaboratorItemType[];
refetchCollaboratorList: () => void;
isFetchingCollaborator: boolean;
getPreLabelList: (per: PermissionValueType) => string[];
getPerLabelList: (per: PermissionValueType) => string[];
};
export type ChildrenProps = {
onOpenAddMember: () => void;
onOpenManageModal: () => void;
MemberListCard: (props: MemberListCardProps) => JSX.Element;
};
type CollaboratorContextType = MemberManagerPropsType & {};
@@ -30,7 +42,7 @@ export const CollaboratorContext = createContext<CollaboratorContextType>({
onDelOneCollaborator: function () {
throw new Error('Function not implemented.');
},
getPreLabelList: function (): string[] {
getPerLabelList: function (): string[] {
throw new Error('Function not implemented.');
},
refetchCollaboratorList: function (): void {
@@ -39,33 +51,36 @@ export const CollaboratorContext = createContext<CollaboratorContextType>({
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
throw new Error('Function not implemented.');
},
isFetchingCollaborator: false
isFetchingCollaborator: false,
permission: new Permission()
});
export const CollaboratorContextProvider = ({
permission,
onGetCollaboratorList,
permissionList,
onUpdateCollaborators,
onDelOneCollaborator,
children
}: MemberManagerInputPropsType & {
children: ReactNode;
children: (props: ChildrenProps) => ReactNode;
}) => {
const {
data: collaboratorList = [],
refetch: refetchCollaboratorList,
isLoading: isFetchingCollaborator
} = useQuery(['collaboratorList'], onGetCollaboratorList);
const onUpdateCollaboratorsThen = async (tmbIds: string[], permission: PermissionValueType) => {
await onUpdateCollaborators(tmbIds, permission);
refetchCollaboratorList();
};
const onDelOneCollaboratorThem = async (tmbId: string) => {
const onDelOneCollaboratorThen = async (tmbId: string) => {
await onDelOneCollaborator(tmbId);
refetchCollaboratorList();
};
const getPreLabelList = useCallback(
const getPerLabelList = useCallback(
(per: PermissionValueType) => {
const Per = new Permission({ per });
const labels: string[] = [];
@@ -91,17 +106,33 @@ export const CollaboratorContextProvider = ({
[permissionList]
);
const {
isOpen: isOpenAddMember,
onOpen: onOpenAddMember,
onClose: onCloseAddMember
} = useDisclosure();
const {
isOpen: isOpenManageModal,
onOpen: onOpenManageModal,
onClose: onCloseManageModal
} = useDisclosure();
const contextValue = {
permission,
onGetCollaboratorList,
collaboratorList,
refetchCollaboratorList,
isFetchingCollaborator,
permissionList,
onUpdateCollaborators: onUpdateCollaboratorsThen,
onDelOneCollaborator: onDelOneCollaboratorThem,
getPreLabelList
onDelOneCollaborator: onDelOneCollaboratorThen,
getPerLabelList
};
return (
<CollaboratorContext.Provider value={contextValue}>{children}</CollaboratorContext.Provider>
<CollaboratorContext.Provider value={contextValue}>
{children({ onOpenAddMember, onOpenManageModal, MemberListCard })}
{isOpenAddMember && <AddMemberModal onClose={onCloseAddMember} />}
{isOpenManageModal && <ManageModal onClose={onCloseManageModal} />}
</CollaboratorContext.Provider>
);
};

View File

@@ -1,99 +0,0 @@
import React, { useState } from 'react';
import { Flex, Box, Button, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { AddMemberModal } from './AddMemberModal';
import { useContextSelector } from 'use-context-selector';
import ManageModal from './ManageModal';
import {
CollaboratorContext,
CollaboratorContextProvider,
MemberManagerInputPropsType
} from './context';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
function MemberManger() {
const { t } = useTranslation();
const {
isOpen: isOpenAddMember,
onOpen: onOpenAddMember,
onClose: onCloseAddMember
} = useDisclosure();
const {
isOpen: isOpenManageModal,
onOpen: onOpenManageModal,
onClose: onCloseManageModal
} = useDisclosure();
const { collaboratorList, isFetchingCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
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>
{/* member list */}
<MyBox
isLoading={isFetchingCollaborator}
mt={2}
bg="myGray.100"
borderRadius="md"
size={'md'}
>
{collaboratorList?.length === 0 ? (
<Box p={3} color="myGray.600" fontSize={'xs'} textAlign={'center'}>
</Box>
) : (
<Flex gap="2" p={1.5}>
{collaboratorList?.map((member) => {
return (
<Tag px="4" py="1.5" bgColor="white" key={member.tmbId} width="fit-content">
<Flex alignItems="center">
<Avatar src={member.avatar} w="24px" />
<TagLabel mx="2">{member.name}</TagLabel>
</Flex>
</Tag>
);
})}
</Flex>
)}
</MyBox>
{isOpenAddMember && <AddMemberModal onClose={onCloseAddMember} />}
{isOpenManageModal && <ManageModal onClose={onCloseManageModal} />}
</>
);
}
function Render(props: MemberManagerInputPropsType) {
return (
<CollaboratorContextProvider {...props}>
<MemberManger />
</CollaboratorContextProvider>
);
}
export default React.memo(Render);

View File

@@ -6,6 +6,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import Avatar from '@/components/Avatar';
const TeamManageModal = dynamic(() => import('../TeamManageModal'));
@@ -46,7 +47,7 @@ const TeamMenu = () => {
<Flex w={'100%'} alignItems={'center'}>
{userInfo?.team ? (
<>
<Image src={userInfo.team.avatar} alt={''} w={'16px'} />
<Avatar src={userInfo.team.avatar} w={'1rem'} />
<Box ml={2}>{userInfo.team.teamName}</Box>
</>
) : (

View File

@@ -1,17 +1,11 @@
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppSchema } from '@fastgpt/global/core/app/type';
export type CreateAppParams = {
name?: string;
avatar?: string;
type?: `${AppTypeEnum}`;
modules: AppSchema['modules'];
edges?: AppSchema['edges'];
};
export type AppUpdateParams = {
parentId?: ParentIdType;
name?: string;
type?: `${AppTypeEnum}`;
type?: AppTypeEnum;
avatar?: string;
intro?: string;
nodes?: AppSchema['modules'];
@@ -23,7 +17,7 @@ export type AppUpdateParams = {
};
export type PostPublishAppProps = {
type: `${AppTypeEnum}`;
type: AppTypeEnum;
nodes: AppSchema['modules'];
edges: AppSchema['edges'];
chatConfig: AppSchema['chatConfig'];

View File

@@ -72,7 +72,7 @@ export type SearchTestResponse = {
searchMode: `${DatasetSearchModeEnum}`;
usingReRank: boolean;
similarity: number;
usingQueryExtension: boolean;
queryExtensionModel?: string;
};
/* =========== training =========== */

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>

View File

@@ -143,7 +143,7 @@ function request(
): any {
/* 去空 */
for (const key in data) {
if (data[key] === null || data[key] === undefined) {
if (data[key] === undefined) {
delete data[key];
}
}

View File

@@ -1,28 +1,25 @@
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import type {
AppDetailType,
AppListItemType,
ChatInputGuideConfigType
} from '@fastgpt/global/core/app/type.d';
import type { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
import { AppUpdateParams, CreateAppParams } from '@/global/core/app/api';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { AppUpdateParams } from '@/global/core/app/api';
import type { CreateAppBody } from '@/pages/api/core/app/create';
import type { ListAppBody } from '@/pages/api/core/app/list';
/**
* 获取模型列表
*/
export const getMyApps = () => GET<AppListItemType[]>('/core/app/list');
export const getMyApps = (data?: ListAppBody) => POST<AppListItemType[]>('/core/app/list', data);
/**
* 创建一个模型
*/
export const postCreateApp = (data: CreateAppParams) => POST<string>('/core/app/create', data);
export const postCreateApp = (data: CreateAppBody) => POST<string>('/core/app/create', data);
export const getMyAppsByTags = (data: {}) => POST(`/proApi/core/chat/team/getApps`, data);
/**
* 根据 ID 删除模型
*/
export const delModelById = (id: string) => DELETE(`/core/app/del?appId=${id}`);
export const delAppById = (id: string) => DELETE(`/core/app/del?appId=${id}`);
/**
* 根据 ID 获取模型

View File

@@ -0,0 +1,11 @@
import { DELETE, GET, POST } from '@/web/common/api/request';
import type { CreateAppFolderBody } from '@/pages/api/core/app/folder/create';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
/* folder */
export const postCreateAppFolder = (data: CreateAppFolderBody) =>
POST('/core/app/folder/create', data);
export const getAppFolderPath = (parentId: ParentIdType) =>
GET<ParentTreePathItemType[]>(`/core/app/folder/path`, { parentId });

View File

@@ -1,3 +1,4 @@
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppDetailType } from '@fastgpt/global/core/app/type.d';
import type { FeishuType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
@@ -6,7 +7,7 @@ import { NullPermission } from '@fastgpt/global/support/permission/constant';
export const defaultApp: AppDetailType = {
_id: '',
name: '应用加载中',
type: 'simple',
type: AppTypeEnum.simple,
avatar: '/icon/logo.svg',
intro: '',
updateTime: Date.now(),

View File

@@ -1,31 +1,25 @@
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { getMyApps } from '@/web/core/app/api';
import { AppListItemType } from '@fastgpt/global/core/app/type';
export type State = {
myApps: AppListItemType[];
loadMyApps: () => Promise<AppListItemType[]>;
loadMyApps: (...arg: Parameters<typeof getMyApps>) => Promise<AppListItemType[]>;
};
export const useAppStore = create<State>()(
devtools(
persist(
immer((set, get) => ({
myApps: [],
async loadMyApps() {
const res = await getMyApps();
set((state) => {
state.myApps = res;
});
return res;
}
})),
{
name: 'appStore',
partialize: (state) => ({})
immer((set, get) => ({
myApps: [],
async loadMyApps(data) {
const res = await getMyApps(data);
set((state) => {
state.myApps = res;
});
return res;
}
)
}))
)
);

View File

@@ -11,7 +11,7 @@ import {
export const appTemplates: (AppItemType & {
avatar: string;
intro: string;
type: `${AppTypeEnum}`;
type: AppTypeEnum;
})[] = [
{
id: 'simpleChat',

View File

@@ -15,7 +15,7 @@ export type SearchTestStoreItemType = {
limit: number;
usingReRank: boolean;
similarity: number;
usingQueryExtension: boolean;
queryExtensionModel?: string;
};
type State = {