fix: disable sending messages if both application and conversation are empty and add loading to all pages (#134)
* feat: add loading to all pages * fix: disable sending messages if both application and conversation are empty * feat: add chatSpin class to Spin of chat
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useGetKnowledgeSearchParams } from '@/hooks/knowledgeHook';
|
||||
import { useGetKnowledgeSearchParams } from '@/hooks/routeHook';
|
||||
import { api_host } from '@/utils/api';
|
||||
import { useSize } from 'ahooks';
|
||||
import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
||||
import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||
import { buildChunkHighlights } from '@/utils/documentUtils';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@@ -46,3 +47,11 @@ export const useGetChunkHighlights = (
|
||||
|
||||
return highlights;
|
||||
};
|
||||
|
||||
export const useSelectChunkListLoading = () => {
|
||||
return useOneNamespaceEffectsLoading('chunkModel', [
|
||||
'create_hunk',
|
||||
'chunk_list',
|
||||
'switch_chunk',
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { useFetchChunkList } from '@/hooks/chunkHooks';
|
||||
import { useDeleteChunkByIds } from '@/hooks/knowledgeHook';
|
||||
import { getOneNamespaceEffectsLoading } from '@/utils/storeUtil';
|
||||
import type { PaginationProps } from 'antd';
|
||||
import { Divider, Flex, Pagination, Space, Spin, message } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSearchParams, useSelector } from 'umi';
|
||||
import ChunkCard from './components/chunk-card';
|
||||
import CreatingModal from './components/chunk-creating-modal';
|
||||
import ChunkToolBar from './components/chunk-toolbar';
|
||||
// import DocumentPreview from './components/document-preview';
|
||||
import classNames from 'classnames';
|
||||
import DocumentPreview from './components/document-preview/preview';
|
||||
import { useHandleChunkCardClick, useSelectDocumentInfo } from './hooks';
|
||||
import {
|
||||
useHandleChunkCardClick,
|
||||
useSelectChunkListLoading,
|
||||
useSelectDocumentInfo,
|
||||
} from './hooks';
|
||||
import { ChunkModelState } from './model';
|
||||
|
||||
import styles from './index.less';
|
||||
interface PayloadType {
|
||||
doc_id: string;
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
const Chunk = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,12 +26,7 @@ const Chunk = () => {
|
||||
const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]);
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data = [], total, pagination } = chunkModel;
|
||||
const effects = useSelector((state: any) => state.loading.effects);
|
||||
const loading = getOneNamespaceEffectsLoading('chunkModel', effects, [
|
||||
'create_hunk',
|
||||
'chunk_list',
|
||||
'switch_chunk',
|
||||
]);
|
||||
const loading = useSelectChunkListLoading();
|
||||
const documentId: string = searchParams.get('doc_id') || '';
|
||||
const [chunkId, setChunkId] = useState<string | undefined>();
|
||||
const { removeChunk } = useDeleteChunkByIds();
|
||||
@@ -40,18 +34,7 @@ const Chunk = () => {
|
||||
const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick();
|
||||
const isPdf = documentInfo.type === 'pdf';
|
||||
|
||||
const getChunkList = useCallback(() => {
|
||||
const payload: PayloadType = {
|
||||
doc_id: documentId,
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: 'chunkModel/chunk_list',
|
||||
payload: {
|
||||
...payload,
|
||||
},
|
||||
});
|
||||
}, [dispatch, documentId]);
|
||||
const getChunkList = useFetchChunkList();
|
||||
|
||||
const handleEditChunk = useCallback(
|
||||
(chunk_id?: string) => {
|
||||
@@ -169,8 +152,8 @@ const Chunk = () => {
|
||||
vertical
|
||||
className={isPdf ? styles.pagePdfWrapper : styles.pageWrapper}
|
||||
>
|
||||
<div className={styles.pageContent}>
|
||||
<Spin spinning={loading} className={styles.spin} size="large">
|
||||
<Spin spinning={loading} className={styles.spin} size="large">
|
||||
<div className={styles.pageContent}>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size={'middle'}
|
||||
@@ -193,8 +176,8 @@ const Chunk = () => {
|
||||
></ChunkCard>
|
||||
))}
|
||||
</Space>
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
<div className={styles.pageFooter}>
|
||||
<Pagination
|
||||
responsive
|
||||
|
||||
@@ -52,8 +52,10 @@ const RenameModal = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldValue('name', initialName);
|
||||
}, [initialName, documentId, form]);
|
||||
if (isModalOpen) {
|
||||
form.setFieldValue('name', initialName);
|
||||
}
|
||||
}, [initialName, documentId, form, isModalOpen]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
import {
|
||||
useFetchKnowledgeBaseConfiguration,
|
||||
useKnowledgeBaseId,
|
||||
} from '@/hooks/knowledgeHook';
|
||||
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
|
||||
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
||||
import {
|
||||
useFetchTenantInfo,
|
||||
useSelectParserList,
|
||||
} from '@/hooks/userSettingHook';
|
||||
import { IKnowledge } from '@/interfaces/database/knowledge';
|
||||
import {
|
||||
getBase64FromUploadFileList,
|
||||
getUploadFileListFromBase64,
|
||||
normFile,
|
||||
} from '@/utils/fileUtil';
|
||||
import { normFile } from '@/utils/fileUtil';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
@@ -26,14 +11,14 @@ import {
|
||||
Select,
|
||||
Slider,
|
||||
Space,
|
||||
Spin,
|
||||
Typography,
|
||||
Upload,
|
||||
UploadFile,
|
||||
} from 'antd';
|
||||
import pick from 'lodash/pick';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'umi';
|
||||
import { LlmModelType } from '../../constant';
|
||||
import {
|
||||
useFetchKnowledgeConfigurationOnMount,
|
||||
useSubmitKnowledgeConfiguration,
|
||||
} from './hooks';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
@@ -41,205 +26,165 @@ const { Title } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const Configuration = () => {
|
||||
const [form] = Form.useForm();
|
||||
const dispatch = useDispatch();
|
||||
const knowledgeBaseId = useKnowledgeBaseId();
|
||||
const loading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']);
|
||||
|
||||
const knowledgeDetails: IKnowledge = useSelector(
|
||||
(state: any) => state.kSModel.knowledgeDetails,
|
||||
);
|
||||
|
||||
const parserList = useSelectParserList();
|
||||
|
||||
const embeddingModelOptions = useSelectLlmOptions();
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
const avatar = await getBase64FromUploadFileList(values.avatar);
|
||||
dispatch({
|
||||
type: 'kSModel/updateKb',
|
||||
payload: {
|
||||
...values,
|
||||
avatar,
|
||||
kb_id: knowledgeBaseId,
|
||||
},
|
||||
});
|
||||
};
|
||||
const { submitKnowledgeConfiguration, submitLoading } =
|
||||
useSubmitKnowledgeConfiguration();
|
||||
const { form, parserList, embeddingModelOptions, loading } =
|
||||
useFetchKnowledgeConfigurationOnMount();
|
||||
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fileList: UploadFile[] = getUploadFileListFromBase64(
|
||||
knowledgeDetails.avatar,
|
||||
);
|
||||
|
||||
form.setFieldsValue({
|
||||
...pick(knowledgeDetails, [
|
||||
'description',
|
||||
'name',
|
||||
'permission',
|
||||
'embd_id',
|
||||
'parser_id',
|
||||
'language',
|
||||
'parser_config.chunk_token_num',
|
||||
]),
|
||||
avatar: fileList,
|
||||
});
|
||||
}, [form, knowledgeDetails]);
|
||||
|
||||
useFetchTenantInfo();
|
||||
useFetchKnowledgeBaseConfiguration();
|
||||
|
||||
useFetchLlmList(LlmModelType.Embedding);
|
||||
|
||||
return (
|
||||
<div className={styles.configurationWrapper}>
|
||||
<Title level={5}>Configuration</Title>
|
||||
<p>Update your knowledge base details especially parsing method here.</p>
|
||||
<Divider></Divider>
|
||||
<Form
|
||||
form={form}
|
||||
name="validateOnly"
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Knowledge base name"
|
||||
rules={[{ required: true }]}
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
form={form}
|
||||
name="validateOnly"
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
onFinish={submitKnowledgeConfiguration}
|
||||
onFinishFailed={onFinishFailed}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="avatar"
|
||||
label="Knowledge base photo"
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={normFile}
|
||||
>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
maxCount={1}
|
||||
beforeUpload={() => false}
|
||||
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Knowledge base name"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<button style={{ border: 0, background: 'none' }} type="button">
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
</button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="Description">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Language"
|
||||
name="language"
|
||||
initialValue={'Chinese'}
|
||||
rules={[{ required: true, message: 'Please input your language!' }]}
|
||||
>
|
||||
<Select placeholder="select your language">
|
||||
<Option value="English">English</Option>
|
||||
<Option value="Chinese">Chinese</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="permission"
|
||||
label="Permissions"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="me">Only me</Radio>
|
||||
<Radio value="team">Team</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="embd_id"
|
||||
label="Embedding Model"
|
||||
rules={[{ required: true }]}
|
||||
tooltip="xx"
|
||||
>
|
||||
<Select
|
||||
placeholder="Please select a country"
|
||||
options={embeddingModelOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="parser_id"
|
||||
label="Knowledge base category"
|
||||
tooltip="xx"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select placeholder="Please select a country">
|
||||
{parserList.map((x) => (
|
||||
<Option value={x.value} key={x.value}>
|
||||
{x.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle dependencies={['parser_id']}>
|
||||
{({ getFieldValue }) => {
|
||||
const parserId = getFieldValue('parser_id');
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="avatar"
|
||||
label="Knowledge base photo"
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={normFile}
|
||||
>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
maxCount={1}
|
||||
beforeUpload={() => false}
|
||||
showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
|
||||
>
|
||||
<button style={{ border: 0, background: 'none' }} type="button">
|
||||
<PlusOutlined />
|
||||
<div style={{ marginTop: 8 }}>Upload</div>
|
||||
</button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="Description">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Language"
|
||||
name="language"
|
||||
initialValue={'Chinese'}
|
||||
rules={[{ required: true, message: 'Please input your language!' }]}
|
||||
>
|
||||
<Select placeholder="select your language">
|
||||
<Option value="English">English</Option>
|
||||
<Option value="Chinese">Chinese</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="permission"
|
||||
label="Permissions"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="me">Only me</Radio>
|
||||
<Radio value="team">Team</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="embd_id"
|
||||
label="Embedding Model"
|
||||
rules={[{ required: true }]}
|
||||
tooltip="xx"
|
||||
>
|
||||
<Select
|
||||
placeholder="Please select a country"
|
||||
options={embeddingModelOptions}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="parser_id"
|
||||
label="Knowledge base category"
|
||||
tooltip="xx"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select placeholder="Please select a country">
|
||||
{parserList.map((x) => (
|
||||
<Option value={x.value} key={x.value}>
|
||||
{x.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle dependencies={['parser_id']}>
|
||||
{({ getFieldValue }) => {
|
||||
const parserId = getFieldValue('parser_id');
|
||||
|
||||
if (parserId === 'naive') {
|
||||
return (
|
||||
<Form.Item label="Chunk token number" tooltip="xxx">
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
if (parserId === 'naive') {
|
||||
return (
|
||||
<Form.Item label="Chunk token number" tooltip="xxx">
|
||||
<Flex gap={20} align="center">
|
||||
<Flex flex={1}>
|
||||
<Form.Item
|
||||
name={['parser_config', 'chunk_token_num']}
|
||||
noStyle
|
||||
initialValue={128}
|
||||
rules={[
|
||||
{ required: true, message: 'Province is required' },
|
||||
]}
|
||||
>
|
||||
<Slider
|
||||
className={styles.variableSlider}
|
||||
max={2048}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={['parser_config', 'chunk_token_num']}
|
||||
noStyle
|
||||
initialValue={128}
|
||||
rules={[
|
||||
{ required: true, message: 'Province is required' },
|
||||
{ required: true, message: 'Street is required' },
|
||||
]}
|
||||
>
|
||||
<Slider className={styles.variableSlider} max={2048} />
|
||||
<InputNumber
|
||||
className={styles.sliderInputNumber}
|
||||
max={2048}
|
||||
min={0}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name={['parser_config', 'chunk_token_num']}
|
||||
noStyle
|
||||
initialValue={128}
|
||||
rules={[
|
||||
{ required: true, message: 'Street is required' },
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
className={styles.sliderInputNumber}
|
||||
max={2048}
|
||||
min={0}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Space>
|
||||
<Button htmlType="reset" size={'middle'}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
size={'middle'}
|
||||
loading={loading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Space>
|
||||
<Button htmlType="reset" size={'middle'}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
size={'middle'}
|
||||
loading={submitLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
useFetchKnowledgeBaseConfiguration,
|
||||
useKnowledgeBaseId,
|
||||
useSelectKnowledgeDetails,
|
||||
useUpdateKnowledge,
|
||||
} from '@/hooks/knowledgeHook';
|
||||
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
|
||||
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
||||
import {
|
||||
useFetchTenantInfo,
|
||||
useSelectParserList,
|
||||
} from '@/hooks/userSettingHook';
|
||||
import {
|
||||
getBase64FromUploadFileList,
|
||||
getUploadFileListFromBase64,
|
||||
} from '@/utils/fileUtil';
|
||||
import { Form, UploadFile } from 'antd';
|
||||
import pick from 'lodash/pick';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { LlmModelType } from '../../constant';
|
||||
|
||||
export const useSubmitKnowledgeConfiguration = () => {
|
||||
const save = useUpdateKnowledge();
|
||||
const knowledgeBaseId = useKnowledgeBaseId();
|
||||
const submitLoading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']);
|
||||
|
||||
const submitKnowledgeConfiguration = useCallback(
|
||||
async (values: any) => {
|
||||
const avatar = await getBase64FromUploadFileList(values.avatar);
|
||||
save({
|
||||
...values,
|
||||
avatar,
|
||||
kb_id: knowledgeBaseId,
|
||||
});
|
||||
},
|
||||
[save, knowledgeBaseId],
|
||||
);
|
||||
|
||||
return { submitKnowledgeConfiguration, submitLoading };
|
||||
};
|
||||
|
||||
export const useFetchKnowledgeConfigurationOnMount = () => {
|
||||
const [form] = Form.useForm();
|
||||
const loading = useOneNamespaceEffectsLoading('kSModel', ['getKbDetail']);
|
||||
|
||||
const knowledgeDetails = useSelectKnowledgeDetails();
|
||||
const parserList = useSelectParserList();
|
||||
const embeddingModelOptions = useSelectLlmOptions();
|
||||
|
||||
useFetchTenantInfo();
|
||||
useFetchKnowledgeBaseConfiguration();
|
||||
useFetchLlmList(LlmModelType.Embedding);
|
||||
|
||||
useEffect(() => {
|
||||
const fileList: UploadFile[] = getUploadFileListFromBase64(
|
||||
knowledgeDetails.avatar,
|
||||
);
|
||||
form.setFieldsValue({
|
||||
...pick(knowledgeDetails, [
|
||||
'description',
|
||||
'name',
|
||||
'permission',
|
||||
'embd_id',
|
||||
'parser_id',
|
||||
'language',
|
||||
'parser_config.chunk_token_num',
|
||||
]),
|
||||
avatar: fileList,
|
||||
});
|
||||
}, [form, knowledgeDetails]);
|
||||
|
||||
return { form, parserList, embeddingModelOptions, loading };
|
||||
};
|
||||
@@ -34,7 +34,7 @@ const model: DvaModel<KSModelState> = {
|
||||
const { data } = yield call(kbService.createKb, payload);
|
||||
const { retcode } = data;
|
||||
if (retcode === 0) {
|
||||
message.success('Created successfully!');
|
||||
message.success('Created!');
|
||||
}
|
||||
return data;
|
||||
},
|
||||
@@ -43,7 +43,7 @@ const model: DvaModel<KSModelState> = {
|
||||
const { retcode } = data;
|
||||
if (retcode === 0) {
|
||||
yield put({ type: 'getKbDetail', payload: { kb_id: payload.kb_id } });
|
||||
message.success('Updated successfully!');
|
||||
message.success('Updated!');
|
||||
}
|
||||
},
|
||||
*getKbDetail({ payload = {} }, { call, put }) {
|
||||
|
||||
@@ -37,9 +37,9 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
|
||||
|
||||
return (
|
||||
<section className={styles.testingControlWrapper}>
|
||||
<p>
|
||||
<div>
|
||||
<b>Retrieval testing</b>
|
||||
</p>
|
||||
</div>
|
||||
<p>Final step! After success, leave the rest to Infiniflow AI.</p>
|
||||
<Divider></Divider>
|
||||
<section>
|
||||
@@ -48,8 +48,6 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
|
||||
layout="vertical"
|
||||
form={form}
|
||||
initialValues={{
|
||||
similarity_threshold: 0.2,
|
||||
vector_similarity_weight: 0.3,
|
||||
top_k: 1024,
|
||||
}}
|
||||
>
|
||||
@@ -81,12 +79,12 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
|
||||
</Form>
|
||||
</section>
|
||||
<section>
|
||||
<p className={styles.historyTitle}>
|
||||
<div className={styles.historyTitle}>
|
||||
<Space size={'middle'}>
|
||||
<HistoryOutlined className={styles.historyIcon} />
|
||||
<b>Test history</b>
|
||||
</Space>
|
||||
</p>
|
||||
</div>
|
||||
<Space
|
||||
direction="vertical"
|
||||
size={'middle'}
|
||||
|
||||
Reference in New Issue
Block a user