mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
Perf: delete app tip; fix: can't stop debug. (#2865)
* fix: variables check * remove log * perf: delete app tip * perf: remove node code * fix: can not stop debug * update version * update version intro * fix: per error * perf: apikey manager * Add permission check * update README
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "4.8.10",
|
||||
"version": "4.8.11",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
@@ -1,46 +1,31 @@
|
||||
### FastGPT V4.8.9
|
||||
### FastGPT V4.8.11 更新说明
|
||||
|
||||
#### 新功能说明
|
||||
1. 新增 - 表单输入节点,允许用户在工作流中让用户输入一些信息。
|
||||
2. 新增 - 循环运行节点,可传入数组进行批量调用,目前最多支持 50 长度的数组串行执行。
|
||||
3. 新增 - 自定义工具变量节点,可以为工具调用子流程完全自定义变量。在构建复杂 Agent 时有帮助。
|
||||
4. 新增 - 节点支持折叠。
|
||||
5. 新增 - 聊天记录滚动加载,不再只加载 30 条。
|
||||
6. 新增 - 工作流增加触摸板优先模式,可以通过工作流右下角按键进行切换。
|
||||
7. 新增 - 沙盒增加字符串转 base64 全局方法(全局变量 strToBase64)。
|
||||
8. 新增 - 支持 Openai o1 模型,需增加模型的 `defaultConfig` 配置,覆盖 `temperature`、`max_tokens` 和 `stream`配置,o1 不支持 stream 模式, 详细可重新拉取 `config.json` 配置文件查看。
|
||||
9. 新增 - AI 对话节点知识库引用,支持配置 role=system 和 role=user,已配置的过自定义提示词的节点将会保持 user 模式,其余用户将转成 system 模式。
|
||||
10. 新增 - 插件支持上传系统文件。
|
||||
11. 新增 - 子应用嵌套调用时,版本锁定。主应用未主动更新版本时,不会取最新版进行执行,保证主应用服务稳定。
|
||||
12. 新增 - 插件输出,支持指定字段作为工具响应。
|
||||
13. 新增 - 支持工作流嵌套子应用时,可以设置`非流模式`,同时简易模式也可以选择工作流作为插件了,简易模式调用子应用时,都将强制使用非流模式。
|
||||
14. 新增 - 调试模式下,子应用调用,支持返回详细运行数据。
|
||||
15. 新增 - 保留所有模式下子应用嵌套调用的日志。
|
||||
16. 新增 - 商业版支持团队成员组,后续将逐渐覆盖工作台和知识库权限。
|
||||
17. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环。
|
||||
18. 优化 - 工作流 handler 性能优化。
|
||||
19. 优化 - 工作流快捷键,避免调试测试时也会触发复制和回退。
|
||||
20. 修复 - 工作流工具调用中修改全局变量后,无法传递到后续流程。
|
||||
21. 优化 - 流输出,切换浏览器 Tab 后仍可以继续输出。
|
||||
22. 优化 - 完善外部文件知识库相关 API
|
||||
23. 修复 - 知识库选择权限问题。
|
||||
24. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常。
|
||||
25. 修复 - createDataset 接口,intro 为赋值。
|
||||
26. 修复 - 对话框渲染性能问题。
|
||||
27. 修复 - 工具调用历史记录存储不正确。
|
||||
|
||||
1. **文件上传**
|
||||
|
||||
支持在简易模式和工作流中上传文档,目前版本,主要以上传文档总结为主,如果需要通过工作流编排实现类似于内容提取、文档对比之类的功能,需要配合代码运行去使用。
|
||||
|
||||
- 在系统配置中可以开启文件上传,对话框中即可上传文件。文件包括文档和图片;是否可上传图片,不再依赖视觉模型决定,而是通过系统配置决定。
|
||||
- 简易模式下,会通过工具调用,由模型决定是否读取文档,所以尽可能选择支持工具调用的模型。
|
||||
- 工作流模式下,可以通过文档解析节点,手动获取解析结果进行后续流程。
|
||||
|
||||
| | |
|
||||
| --------------------------------------------------------- | --------------------------------------------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
2. **AI 对话模型,启用视觉模型**
|
||||
|
||||
之前有反馈,工作流中所有模型都会强制使用视觉模式,浪费了资源,目前可以手动关闭。开启后会自动获取对话框上传的图片和“用户问题”中的图片链接。
|
||||
|
||||

|
||||
|
||||
3. **清空对话引导**
|
||||
|
||||

|
||||
|
||||
#### 优化说明
|
||||
|
||||
1. 优化 - 对话框懒加载。
|
||||
2. 优化 - i18n 翻译。
|
||||
3. 优化 - 减少工具调用结果的存储,不再实际存储完整响应,避免超出存储限制。
|
||||
4. 优化 - QA 拆分支持自定义 chunk 大小,并优化 gpt4o-mini 拆分时,chunk 太大导致生成内容很少的问题。
|
||||
|
||||
#### 问题修复
|
||||
|
||||
1. 修复 - 删除应用后回到聊天选择最后一次对话的应用为删除的应用时提示无该应用问题。
|
||||
2. 修复 - 插件运行获取历史记录后无法更新到页面。
|
||||
3. 修复 - 插件默认值无法正常显示。
|
||||
4. 修复 - 工具调用温度和最大回复值未生效。
|
||||
5. 修复 - 知识库文件上传进度更新可能异常。
|
||||
6. 修复 - 知识库 rebuilding 时候,页面总是刷新到第一页。
|
||||
7. 修复 - 函数调用模式,assistant role 中,GPT 模型必须传入 content 参数。(不影响大部分模型,目前基本都改用用 ToolChoice 模式,FC 模式已弃用)。
|
||||
8. 修复 - docs: Repair and supplement document content xinference.md。
|
||||
9. 修复 - 知识库列表右下角类型的名称上下居中。
|
||||
10. 修复 - 知识库 list openapi 鉴权问题。
|
||||
|
@@ -24,7 +24,6 @@ import {
|
||||
putOpenApiKey
|
||||
} from '@/web/support/openapi/api';
|
||||
import type { EditApiKeyProps } from '@/global/support/openapi/api.d';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
@@ -33,7 +32,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
@@ -61,13 +60,10 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
type: 'delete',
|
||||
content: t('workflow:delete_api')
|
||||
content: t('common:delete_api')
|
||||
});
|
||||
|
||||
const { mutate: onclickRemove, isLoading: isDeleting } = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
return delOpenApiById(id);
|
||||
},
|
||||
const { runAsync: onclickRemove } = useRequest2(delOpenApiById, {
|
||||
onSuccess() {
|
||||
refetch();
|
||||
}
|
||||
@@ -75,9 +71,12 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
|
||||
const {
|
||||
data: apiKeys = [],
|
||||
isLoading: isGetting,
|
||||
refetch
|
||||
} = useQuery(['getOpenApiKeys', appId], () => getOpenApiKeys({ appId }));
|
||||
loading: isGetting,
|
||||
run: refetch
|
||||
} = useRequest2(() => getOpenApiKeys({ appId }), {
|
||||
manual: false,
|
||||
refreshDeps: [appId]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBaseUrl(feConfigs?.customApiDomain || `${location.origin}/api`);
|
||||
@@ -85,7 +84,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
isLoading={isGetting || isDeleting}
|
||||
isLoading={isGetting}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
|
@@ -52,8 +52,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
}
|
||||
]
|
||||
: []),
|
||||
// ...(feConfigs?.show_pay && userInfo?.team?.permission.hasWritePer
|
||||
...(feConfigs?.show_pay || userInfo?.team?.permission.hasWritePer
|
||||
...(feConfigs?.show_pay && userInfo?.team?.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
icon: 'support/bill/payRecordLight',
|
||||
@@ -62,8 +61,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
}
|
||||
]
|
||||
: []),
|
||||
|
||||
...(feConfigs?.show_promotion
|
||||
...(feConfigs?.show_promotion && userInfo?.team?.permission.isOwner
|
||||
? [
|
||||
{
|
||||
icon: 'support/account/promotionLight',
|
||||
@@ -72,7 +70,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(userInfo?.team?.permission.hasWritePer
|
||||
...(userInfo?.team?.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
icon: 'support/outlink/apikeyLight',
|
||||
|
@@ -1,64 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||
import { FastGPTProUrl } from '@fastgpt/service/common/system/constants';
|
||||
import { POST } from '@fastgpt/service/common/api/plusRequest';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoMemberGroupModel } from '@fastgpt/service/support/permission/memberGroup/memberGroupSchema';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
|
||||
/*
|
||||
1. 给每个 team 创建一个默认的 group
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
await authCert({ req, authRoot: true });
|
||||
|
||||
const teamList = await MongoTeam.find({}, '_id');
|
||||
console.log('Total team', teamList.length);
|
||||
let success = 0;
|
||||
|
||||
async function createGroup(teamId: string) {
|
||||
try {
|
||||
await MongoMemberGroupModel.updateOne(
|
||||
{
|
||||
teamId,
|
||||
name: DefaultGroupName
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
teamId: teamId,
|
||||
name: DefaultGroupName
|
||||
}
|
||||
},
|
||||
{
|
||||
upsert: true
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await delay(500);
|
||||
return createGroup(teamId);
|
||||
}
|
||||
}
|
||||
for await (const team of teamList) {
|
||||
await createGroup(team._id);
|
||||
console.log(++success);
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
message: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
@@ -413,32 +413,7 @@ export const useWorkflow = () => {
|
||||
});
|
||||
|
||||
/* node */
|
||||
const handleRemoveNode = useMemoizedFn((nodeId: string) => {
|
||||
// If the node has child nodes, remove the child nodes
|
||||
const childNodes = nodes.filter((n) => n.data.parentNodeId === nodeId);
|
||||
if (childNodes.length > 0) {
|
||||
const childNodeIds = childNodes.map((node) => node.id);
|
||||
const childEdges = edges.filter(
|
||||
(edge) => childNodeIds.includes(edge.source) || childNodeIds.includes(edge.target)
|
||||
);
|
||||
|
||||
onNodesChange(
|
||||
childNodes.map<NodeRemoveChange>((node) => ({
|
||||
type: 'remove',
|
||||
id: node.id
|
||||
}))
|
||||
);
|
||||
onEdgesChange(
|
||||
childEdges.map<EdgeRemoveChange>((edge) => ({
|
||||
type: 'remove',
|
||||
id: edge.id
|
||||
}))
|
||||
);
|
||||
}
|
||||
onNodesChange([{ type: 'remove', id: nodeId }]);
|
||||
|
||||
return;
|
||||
});
|
||||
const handleSelectNode = useMemoizedFn((change: NodeSelectionChange) => {
|
||||
// If the node is not selected and the Ctrl key is pressed, select the node
|
||||
if (change.selected === false && isDowningCtrl) {
|
||||
@@ -618,9 +593,8 @@ export const useWorkflow = () => {
|
||||
|
||||
useKeyPress(['Delete', 'Backspace'], (e) => {
|
||||
if (!mouseInCanvas) return;
|
||||
const selectedNodes = nodes.filter((node) => node.selected);
|
||||
const selectedEdges = edges.filter((edge) => edge.selected);
|
||||
|
||||
const selectedNodes = nodes.filter((node) => node.selected);
|
||||
if (selectedNodes.length > 0) {
|
||||
for (const node of selectedNodes) {
|
||||
if (node.data.forbidDelete) {
|
||||
@@ -631,13 +605,27 @@ export const useWorkflow = () => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Computed deleted node and its edges
|
||||
const removedNodeId = node.id;
|
||||
handleRemoveNode(removedNodeId);
|
||||
const edgesToRemove = edges.filter(
|
||||
const removedNodeEdges = edges.filter(
|
||||
(edge) => edge.source === removedNodeId || edge.target === removedNodeId
|
||||
);
|
||||
|
||||
const childNodes = nodes.filter((n) => n.data.parentNodeId === removedNodeId);
|
||||
const childNodeIds = childNodes.map((node) => node.id);
|
||||
const childEdges = edges.filter(
|
||||
(edge) => childNodeIds.includes(edge.source) || childNodeIds.includes(edge.target)
|
||||
);
|
||||
|
||||
// Delete
|
||||
onNodesChange(
|
||||
[removedNodeId, ...childNodeIds].map((nodeId) => ({
|
||||
type: 'remove',
|
||||
id: nodeId
|
||||
}))
|
||||
);
|
||||
onEdgesChange(
|
||||
edgesToRemove.map((edge) => ({
|
||||
[...removedNodeEdges, ...childEdges].map((edge) => ({
|
||||
type: 'remove',
|
||||
id: edge.id
|
||||
}))
|
||||
@@ -645,6 +633,8 @@ export const useWorkflow = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete selected edges
|
||||
const selectedEdges = edges.filter((edge) => edge.selected);
|
||||
if (selectedEdges.length > 0) {
|
||||
onEdgesChange(
|
||||
selectedEdges.map((edge) => ({
|
||||
|
@@ -606,10 +606,10 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const onStopNodeDebug = useContextSelector(WorkflowContext, (v) => v.onStopNodeDebug);
|
||||
const onNextNodeDebug = useContextSelector(WorkflowContext, (v) => v.onNextNodeDebug);
|
||||
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
|
||||
const { onChangeNode, onStopNodeDebug, onNextNodeDebug, workflowDebugData } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('common:core.workflow.Confirm stop debug')
|
||||
@@ -741,11 +741,9 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
<ConfirmModal />
|
||||
</>
|
||||
) : null;
|
||||
}, [
|
||||
ConfirmModal,
|
||||
debugResult,
|
||||
nodeId,
|
||||
onChangeNode,
|
||||
@@ -753,8 +751,13 @@ const NodeDebugResponse = React.memo(function NodeDebugResponse({
|
||||
onStopNodeDebug,
|
||||
openConfirm,
|
||||
t,
|
||||
workflowDebugData?.nextRunNodes
|
||||
workflowDebugData
|
||||
]);
|
||||
|
||||
return <>{RenderStatus}</>;
|
||||
return (
|
||||
<>
|
||||
{RenderStatus}
|
||||
<ConfirmModal />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@@ -160,7 +160,7 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
});
|
||||
|
||||
const { openConfirm: openConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({
|
||||
content: t('app:confirm_del_app_tip'),
|
||||
content: t('app:confirm_del_app_tip', { name: appDetail.name }),
|
||||
type: 'delete'
|
||||
});
|
||||
const { runAsync: deleteApp } = useRequest2(
|
||||
|
@@ -365,7 +365,7 @@ const ListItem = () => {
|
||||
undefined,
|
||||
app.type === AppTypeEnum.folder
|
||||
? t('app:confirm_delete_folder_tip')
|
||||
: t('app:confirm_del_app_tip')
|
||||
: t('app:confirm_del_app_tip', { name: app.name })
|
||||
)()
|
||||
}
|
||||
]
|
||||
|
@@ -82,7 +82,7 @@ const Dataset = () => {
|
||||
|
||||
const RenderSearchInput = useMemo(
|
||||
() => (
|
||||
<InputGroup maxW={['auto', '250px']} pr={[0, 4]}>
|
||||
<InputGroup maxW={['auto', '250px']}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
<MyIcon color={'myGray.600'} name={'common/searchLight'} w={'1rem'} />
|
||||
</InputLeftElement>
|
||||
@@ -100,6 +100,7 @@ const Dataset = () => {
|
||||
),
|
||||
[searchKey, setSearchKey, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
isLoading={myDatasets.length === 0 && isFetchingDatasets}
|
||||
@@ -110,7 +111,7 @@ const Dataset = () => {
|
||||
>
|
||||
<Flex pt={[4, 6]} pl={3} pr={[3, 10]}>
|
||||
<Flex flexGrow={1} flexDirection="column">
|
||||
<Flex alignItems={'flex-start'} justifyContent={'space-between'}>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<ParentPaths
|
||||
paths={paths}
|
||||
FirstPathDom={
|
||||
@@ -138,54 +139,56 @@ const Dataset = () => {
|
||||
{isPc && RenderSearchInput}
|
||||
|
||||
{userInfo?.team?.permission.hasWritePer && (
|
||||
<MyMenu
|
||||
offset={[0, 10]}
|
||||
width={120}
|
||||
iconSize="2rem"
|
||||
iconRadius="6px"
|
||||
placement="bottom-end"
|
||||
Button={
|
||||
<Button variant={'primary'} px="0">
|
||||
<Flex alignItems={'center'} px={5}>
|
||||
<AddIcon mr={2} />
|
||||
<Box>{t('common:common.Create New')}</Box>
|
||||
</Flex>
|
||||
</Button>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/dataset/commonDatasetColor',
|
||||
label: t('dataset:common_dataset'),
|
||||
description: t('dataset:common_dataset_desc'),
|
||||
onClick: () => setCreateDatasetType(DatasetTypeEnum.dataset)
|
||||
},
|
||||
{
|
||||
icon: 'core/dataset/websiteDatasetColor',
|
||||
label: t('dataset:website_dataset'),
|
||||
description: t('dataset:website_dataset_desc'),
|
||||
onClick: () => setCreateDatasetType(DatasetTypeEnum.websiteDataset)
|
||||
},
|
||||
{
|
||||
icon: 'core/dataset/externalDatasetColor',
|
||||
label: t('dataset:external_file'),
|
||||
description: t('dataset:external_file_dataset_desc'),
|
||||
onClick: () => setCreateDatasetType(DatasetTypeEnum.externalFile)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: FolderIcon,
|
||||
label: t('common:Folder'),
|
||||
onClick: () => setEditFolderData({})
|
||||
}
|
||||
]
|
||||
<Box pl={[0, 4]}>
|
||||
<MyMenu
|
||||
offset={[0, 10]}
|
||||
width={120}
|
||||
iconSize="2rem"
|
||||
iconRadius="6px"
|
||||
placement="bottom-end"
|
||||
Button={
|
||||
<Button variant={'primary'} px="0">
|
||||
<Flex alignItems={'center'} px={5}>
|
||||
<AddIcon mr={2} />
|
||||
<Box>{t('common:common.Create New')}</Box>
|
||||
</Flex>
|
||||
</Button>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/dataset/commonDatasetColor',
|
||||
label: t('dataset:common_dataset'),
|
||||
description: t('dataset:common_dataset_desc'),
|
||||
onClick: () => setCreateDatasetType(DatasetTypeEnum.dataset)
|
||||
},
|
||||
{
|
||||
icon: 'core/dataset/websiteDatasetColor',
|
||||
label: t('dataset:website_dataset'),
|
||||
description: t('dataset:website_dataset_desc'),
|
||||
onClick: () => setCreateDatasetType(DatasetTypeEnum.websiteDataset)
|
||||
},
|
||||
{
|
||||
icon: 'core/dataset/externalDatasetColor',
|
||||
label: t('dataset:external_file'),
|
||||
description: t('dataset:external_file_dataset_desc'),
|
||||
onClick: () => setCreateDatasetType(DatasetTypeEnum.externalFile)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: FolderIcon,
|
||||
label: t('common:Folder'),
|
||||
onClick: () => setEditFolderData({})
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
|
Reference in New Issue
Block a user