From 78727c880901bd2d03f575938d6f04e79baba382 Mon Sep 17 00:00:00 2001
From: balibabu <cike8899@users.noreply.github.com>
Date: Wed, 20 Mar 2024 11:13:51 +0800
Subject: [PATCH] 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
---
 web/src/components/rename-modal/index.tsx     |   6 +-
 web/src/hooks/chunkHooks.ts                   |  24 ++
 web/src/hooks/knowledgeHook.ts                |  45 ++-
 web/src/hooks/llmHooks.ts                     |   2 +-
 web/src/hooks/routeHook.ts                    |  14 +-
 .../components/document-preview/hooks.ts      |   2 +-
 .../components/knowledge-chunk/hooks.ts       |   9 +
 .../components/knowledge-chunk/index.tsx      |  43 +--
 .../knowledge-file/rename-modal/index.tsx     |   6 +-
 .../knowledge-setting/configuration.tsx       | 343 ++++++++----------
 .../components/knowledge-setting/hooks.ts     |  73 ++++
 .../components/knowledge-setting/model.ts     |   4 +-
 .../testing-control/index.tsx                 |  10 +-
 .../assistant-setting.tsx                     |   7 +-
 web/src/pages/chat/chat-container/index.tsx   |  19 +-
 web/src/pages/chat/hooks.ts                   |  12 +
 web/src/pages/chat/index.less                 |   8 +
 web/src/pages/chat/index.tsx                  | 126 ++++---
 web/src/pages/knowledge/index.tsx             |  34 +-
 web/src/pages/user-setting/hooks.ts           |   5 +-
 .../pages/user-setting/setting-model/hooks.ts |   9 +
 .../user-setting/setting-model/index.tsx      |  30 +-
 .../user-setting/setting-profile/index.tsx    | 261 ++++++-------
 web/src/utils/fileUtil.ts                     |  10 +-
 24 files changed, 629 insertions(+), 473 deletions(-)
 create mode 100644 web/src/hooks/chunkHooks.ts
 create mode 100644 web/src/pages/add-knowledge/components/knowledge-setting/hooks.ts

diff --git a/web/src/components/rename-modal/index.tsx b/web/src/components/rename-modal/index.tsx
index ba902c5..9802ff8 100644
--- a/web/src/components/rename-modal/index.tsx
+++ b/web/src/components/rename-modal/index.tsx
@@ -41,8 +41,10 @@ const RenameModal = ({
   };
 
   useEffect(() => {
-    form.setFieldValue('name', initialName);
-  }, [initialName, form]);
+    if (visible) {
+      form.setFieldValue('name', initialName);
+    }
+  }, [initialName, form, visible]);
 
   return (
     <Modal
diff --git a/web/src/hooks/chunkHooks.ts b/web/src/hooks/chunkHooks.ts
new file mode 100644
index 0000000..377192f
--- /dev/null
+++ b/web/src/hooks/chunkHooks.ts
@@ -0,0 +1,24 @@
+import { useCallback } from 'react';
+import { useDispatch } from 'umi';
+import { useGetKnowledgeSearchParams } from './routeHook';
+
+interface PayloadType {
+  doc_id: string;
+  keywords?: string;
+}
+
+export const useFetchChunkList = () => {
+  const dispatch = useDispatch();
+  const { documentId } = useGetKnowledgeSearchParams();
+
+  const fetchChunkList = useCallback(() => {
+    dispatch({
+      type: 'chunkModel/chunk_list',
+      payload: {
+        doc_id: documentId,
+      },
+    });
+  }, [dispatch, documentId]);
+
+  return fetchChunkList;
+};
diff --git a/web/src/hooks/knowledgeHook.ts b/web/src/hooks/knowledgeHook.ts
index bb874e6..1ef5167 100644
--- a/web/src/hooks/knowledgeHook.ts
+++ b/web/src/hooks/knowledgeHook.ts
@@ -1,8 +1,9 @@
 import showDeleteConfirm from '@/components/deleting-confirm';
-import { KnowledgeSearchParams } from '@/constants/knowledge';
 import { IKnowledge } from '@/interfaces/database/knowledge';
 import { useCallback, useEffect, useMemo } from 'react';
 import { useDispatch, useSearchParams, useSelector } from 'umi';
+import { useGetKnowledgeSearchParams } from './routeHook';
+import { useOneNamespaceEffectsLoading } from './storeHooks';
 
 export const useKnowledgeBaseId = (): string => {
   const [searchParams] = useSearchParams();
@@ -11,17 +12,6 @@ export const useKnowledgeBaseId = (): string => {
   return knowledgeBaseId || '';
 };
 
-export const useGetKnowledgeSearchParams = () => {
-  const [currentQueryParameters] = useSearchParams();
-
-  return {
-    documentId:
-      currentQueryParameters.get(KnowledgeSearchParams.DocumentId) || '',
-    knowledgeId:
-      currentQueryParameters.get(KnowledgeSearchParams.KnowledgeId) || '',
-  };
-};
-
 export const useDeleteDocumentById = (): {
   removeDocument: (documentId: string) => Promise<number>;
 } => {
@@ -135,8 +125,9 @@ export const useFetchKnowledgeBaseConfiguration = () => {
 
 export const useFetchKnowledgeList = (
   shouldFilterListWithoutDocument: boolean = false,
-): IKnowledge[] => {
+): { list: IKnowledge[]; loading: boolean } => {
   const dispatch = useDispatch();
+  const loading = useOneNamespaceEffectsLoading('knowledgeModel', ['getList']);
 
   const knowledgeModel = useSelector((state: any) => state.knowledgeModel);
   const { data = [] } = knowledgeModel;
@@ -156,7 +147,7 @@ export const useFetchKnowledgeList = (
     fetchList();
   }, [fetchList]);
 
-  return list;
+  return { list, loading };
 };
 
 export const useSelectFileThumbnails = () => {
@@ -189,3 +180,29 @@ export const useFetchFileThumbnails = (docIds?: Array<string>) => {
 
   return { fileThumbnails, fetchFileThumbnails };
 };
+
+//#region knowledge configuration
+
+export const useUpdateKnowledge = () => {
+  const dispatch = useDispatch();
+
+  const saveKnowledgeConfiguration = useCallback(
+    (payload: any) => {
+      dispatch({
+        type: 'kSModel/updateKb',
+        payload,
+      });
+    },
+    [dispatch],
+  );
+
+  return saveKnowledgeConfiguration;
+};
+
+export const useSelectKnowledgeDetails = () => {
+  const knowledgeDetails: IKnowledge = useSelector(
+    (state: any) => state.kSModel.knowledgeDetails,
+  );
+  return knowledgeDetails;
+};
+//#endregion
diff --git a/web/src/hooks/llmHooks.ts b/web/src/hooks/llmHooks.ts
index c48974c..7c3e243 100644
--- a/web/src/hooks/llmHooks.ts
+++ b/web/src/hooks/llmHooks.ts
@@ -8,8 +8,8 @@ import { useCallback, useEffect, useMemo } from 'react';
 import { useDispatch, useSelector } from 'umi';
 
 export const useFetchLlmList = (
-  isOnMountFetching: boolean = true,
   modelType?: LlmModelType,
+  isOnMountFetching: boolean = true,
 ) => {
   const dispatch = useDispatch();
 
diff --git a/web/src/hooks/routeHook.ts b/web/src/hooks/routeHook.ts
index e51983c..f03e4a0 100644
--- a/web/src/hooks/routeHook.ts
+++ b/web/src/hooks/routeHook.ts
@@ -1,4 +1,5 @@
-import { useLocation } from 'umi';
+import { KnowledgeSearchParams } from '@/constants/knowledge';
+import { useLocation, useSearchParams } from 'umi';
 
 export enum SegmentIndex {
   Second = '2',
@@ -19,3 +20,14 @@ export const useSecondPathName = () => {
 export const useThirdPathName = () => {
   return useSegmentedPathName(SegmentIndex.Third);
 };
+
+export const useGetKnowledgeSearchParams = () => {
+  const [currentQueryParameters] = useSearchParams();
+
+  return {
+    documentId:
+      currentQueryParameters.get(KnowledgeSearchParams.DocumentId) || '',
+    knowledgeId:
+      currentQueryParameters.get(KnowledgeSearchParams.KnowledgeId) || '',
+  };
+};
diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts
index 1d0917a..3183a1d 100644
--- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts
+++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hooks.ts
@@ -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';
diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts b/web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts
index edd256b..59bf2fd 100644
--- a/web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts
+++ b/web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts
@@ -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',
+  ]);
+};
diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx
index ab9bb20..d63e70e 100644
--- a/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx
+++ b/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx
@@ -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
diff --git a/web/src/pages/add-knowledge/components/knowledge-file/rename-modal/index.tsx b/web/src/pages/add-knowledge/components/knowledge-file/rename-modal/index.tsx
index 13109e6..a80d7f4 100644
--- a/web/src/pages/add-knowledge/components/knowledge-file/rename-modal/index.tsx
+++ b/web/src/pages/add-knowledge/components/knowledge-file/rename-modal/index.tsx
@@ -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
diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx b/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx
index f0e7e70..7d50cfb 100644
--- a/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx
+++ b/web/src/pages/add-knowledge/components/knowledge-setting/configuration.tsx
@@ -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 }]}
-        >
-          <Input />
-        </Form.Item>
-        <Form.Item
-          name="avatar"
-          label="Knowledge base photo"
-          valuePropName="fileList"
-          getValueFromEvent={normFile}
+      <Spin spinning={loading}>
+        <Form
+          form={form}
+          name="validateOnly"
+          layout="vertical"
+          autoComplete="off"
+          onFinish={submitKnowledgeConfiguration}
+          onFinishFailed={onFinishFailed}
         >
-          <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>
   );
 };
diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/hooks.ts b/web/src/pages/add-knowledge/components/knowledge-setting/hooks.ts
new file mode 100644
index 0000000..008979d
--- /dev/null
+++ b/web/src/pages/add-knowledge/components/knowledge-setting/hooks.ts
@@ -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 };
+};
diff --git a/web/src/pages/add-knowledge/components/knowledge-setting/model.ts b/web/src/pages/add-knowledge/components/knowledge-setting/model.ts
index 1f44996..29126cc 100644
--- a/web/src/pages/add-knowledge/components/knowledge-setting/model.ts
+++ b/web/src/pages/add-knowledge/components/knowledge-setting/model.ts
@@ -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 }) {
diff --git a/web/src/pages/add-knowledge/components/knowledge-testing/testing-control/index.tsx b/web/src/pages/add-knowledge/components/knowledge-testing/testing-control/index.tsx
index 791f1db..637fd89 100644
--- a/web/src/pages/add-knowledge/components/knowledge-testing/testing-control/index.tsx
+++ b/web/src/pages/add-knowledge/components/knowledge-testing/testing-control/index.tsx
@@ -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'}
diff --git a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx
index 65c7dae..329c121 100644
--- a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx
+++ b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx
@@ -1,14 +1,13 @@
+import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
+import { PlusOutlined } from '@ant-design/icons';
 import { Form, Input, Select, Upload } from 'antd';
-
 import classNames from 'classnames';
 import { ISegmentedContentProps } from '../interface';
 
-import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
-import { PlusOutlined } from '@ant-design/icons';
 import styles from './index.less';
 
 const AssistantSetting = ({ show }: ISegmentedContentProps) => {
-  const knowledgeList = useFetchKnowledgeList(true);
+  const { list: knowledgeList } = useFetchKnowledgeList(true);
   const knowledgeOptions = knowledgeList.map((x) => ({
     label: x.name,
     value: x.id,
diff --git a/web/src/pages/chat/chat-container/index.tsx b/web/src/pages/chat/chat-container/index.tsx
index e7cd63e..17a9469 100644
--- a/web/src/pages/chat/chat-container/index.tsx
+++ b/web/src/pages/chat/chat-container/index.tsx
@@ -30,6 +30,7 @@ import {
   useClickDrawer,
   useFetchConversationOnMount,
   useGetFileIcon,
+  useGetSendButtonDisabled,
   useSendMessage,
 } from '../hooks';
 
@@ -248,11 +249,15 @@ const ChatContainer = () => {
     addNewestConversation,
     removeLatestMessage,
   } = useFetchConversationOnMount();
-  const { handleInputChange, handlePressEnter, value, loading } =
-    useSendMessage(conversation, addNewestConversation, removeLatestMessage);
+  const {
+    handleInputChange,
+    handlePressEnter,
+    value,
+    loading: sendLoading,
+  } = useSendMessage(conversation, addNewestConversation, removeLatestMessage);
   const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
     useClickDrawer();
-
+  const disabled = useGetSendButtonDisabled();
   useGetFileIcon();
 
   return (
@@ -284,8 +289,14 @@ const ChatContainer = () => {
           size="large"
           placeholder="Message Resume Assistant..."
           value={value}
+          disabled={disabled}
           suffix={
-            <Button type="primary" onClick={handlePressEnter} loading={loading}>
+            <Button
+              type="primary"
+              onClick={handlePressEnter}
+              loading={sendLoading}
+              disabled={disabled}
+            >
               Send
             </Button>
           }
diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts
index 877c91a..6da74c4 100644
--- a/web/src/pages/chat/hooks.ts
+++ b/web/src/pages/chat/hooks.ts
@@ -767,4 +767,16 @@ export const useClickDrawer = () => {
   };
 };
 
+export const useSelectDialogListLoading = () => {
+  return useOneNamespaceEffectsLoading('chatModel', ['listDialog']);
+};
+export const useSelectConversationListLoading = () => {
+  return useOneNamespaceEffectsLoading('chatModel', ['listConversation']);
+};
+
+export const useGetSendButtonDisabled = () => {
+  const { dialogId, conversationId } = useGetChatSearchParams();
+
+  return dialogId === '' && conversationId === '';
+};
 //#endregion
diff --git a/web/src/pages/chat/index.less b/web/src/pages/chat/index.less
index 57eb15a..7372f5e 100644
--- a/web/src/pages/chat/index.less
+++ b/web/src/pages/chat/index.less
@@ -41,6 +41,14 @@
     overflow: auto;
   }
 
+  .chatSpin {
+    :global(.ant-spin-container) {
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+    }
+  }
+
   .chatTitleCard {
     :global(.ant-card-body) {
       padding: 8px;
diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx
index b6ee17a..88c3bca 100644
--- a/web/src/pages/chat/index.tsx
+++ b/web/src/pages/chat/index.tsx
@@ -1,5 +1,4 @@
 import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg';
-import { useSetModalState } from '@/hooks/commonHooks';
 import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons';
 import {
   Avatar,
@@ -10,6 +9,7 @@ import {
   Flex,
   MenuProps,
   Space,
+  Spin,
   Tag,
 } from 'antd';
 import { MenuItemProps } from 'antd/lib/menu/MenuItem';
@@ -29,8 +29,9 @@ import {
   useRemoveDialog,
   useRenameConversation,
   useSelectConversationList,
+  useSelectConversationListLoading,
+  useSelectDialogListLoading,
   useSelectFirstDialogOnMount,
-  useSetCurrentDialog,
 } from './hooks';
 
 import RenameModal from '@/components/rename-modal';
@@ -38,8 +39,6 @@ import styles from './index.less';
 
 const Chat = () => {
   const dialogList = useSelectFirstDialogOnMount();
-  const { visible, hideModal, showModal } = useSetModalState();
-  const { setCurrentDialog, currentDialog } = useSetCurrentDialog();
   const { onRemoveDialog } = useRemoveDialog();
   const { onRemoveConversation } = useRemoveConversation();
   const { handleClickDialog } = useClickDialogCard();
@@ -70,6 +69,8 @@ const Chat = () => {
     hideDialogEditModal,
     showDialogEditModal,
   } = useEditDialog();
+  const dialogLoading = useSelectDialogListLoading();
+  const conversationLoading = useSelectConversationListLoading();
 
   useFetchDialogOnMount(dialogId, true);
 
@@ -204,35 +205,39 @@ const Chat = () => {
           </Button>
           <Divider></Divider>
           <Flex className={styles.chatAppContent} vertical gap={10}>
-            {dialogList.map((x) => (
-              <Card
-                key={x.id}
-                hoverable
-                className={classNames(styles.chatAppCard, {
-                  [styles.chatAppCardSelected]: dialogId === x.id,
-                })}
-                onMouseEnter={handleAppCardEnter(x.id)}
-                onMouseLeave={handleItemLeave}
-                onClick={handleDialogCardClick(x.id)}
-              >
-                <Flex justify="space-between" align="center">
-                  <Space size={15}>
-                    <Avatar src={x.icon} shape={'square'} />
-                    <section>
-                      <b>{x.name}</b>
-                      <div>{x.description}</div>
-                    </section>
-                  </Space>
-                  {activated === x.id && (
-                    <section>
-                      <Dropdown menu={{ items: buildAppItems(x.id) }}>
-                        <ChatAppCube className={styles.cubeIcon}></ChatAppCube>
-                      </Dropdown>
-                    </section>
-                  )}
-                </Flex>
-              </Card>
-            ))}
+            <Spin spinning={dialogLoading} wrapperClassName={styles.chatSpin}>
+              {dialogList.map((x) => (
+                <Card
+                  key={x.id}
+                  hoverable
+                  className={classNames(styles.chatAppCard, {
+                    [styles.chatAppCardSelected]: dialogId === x.id,
+                  })}
+                  onMouseEnter={handleAppCardEnter(x.id)}
+                  onMouseLeave={handleItemLeave}
+                  onClick={handleDialogCardClick(x.id)}
+                >
+                  <Flex justify="space-between" align="center">
+                    <Space size={15}>
+                      <Avatar src={x.icon} shape={'square'} />
+                      <section>
+                        <b>{x.name}</b>
+                        <div>{x.description}</div>
+                      </section>
+                    </Space>
+                    {activated === x.id && (
+                      <section>
+                        <Dropdown menu={{ items: buildAppItems(x.id) }}>
+                          <ChatAppCube
+                            className={styles.cubeIcon}
+                          ></ChatAppCube>
+                        </Dropdown>
+                      </section>
+                    )}
+                  </Flex>
+                </Card>
+              ))}
+            </Spin>
           </Flex>
         </Flex>
       </Flex>
@@ -254,29 +259,38 @@ const Chat = () => {
           </Flex>
           <Divider></Divider>
           <Flex vertical gap={10} className={styles.chatTitleContent}>
-            {conversationList.map((x) => (
-              <Card
-                key={x.id}
-                hoverable
-                onClick={handleConversationCardClick(x.id)}
-                onMouseEnter={handleConversationCardEnter(x.id)}
-                onMouseLeave={handleConversationItemLeave}
-                className={classNames(styles.chatTitleCard, {
-                  [styles.chatTitleCardSelected]: x.id === conversationId,
-                })}
-              >
-                <Flex justify="space-between" align="center">
-                  <div>{x.name}</div>
-                  {conversationActivated === x.id && x.id !== '' && (
-                    <section>
-                      <Dropdown menu={{ items: buildConversationItems(x.id) }}>
-                        <ChatAppCube className={styles.cubeIcon}></ChatAppCube>
-                      </Dropdown>
-                    </section>
-                  )}
-                </Flex>
-              </Card>
-            ))}
+            <Spin
+              spinning={conversationLoading}
+              wrapperClassName={styles.chatSpin}
+            >
+              {conversationList.map((x) => (
+                <Card
+                  key={x.id}
+                  hoverable
+                  onClick={handleConversationCardClick(x.id)}
+                  onMouseEnter={handleConversationCardEnter(x.id)}
+                  onMouseLeave={handleConversationItemLeave}
+                  className={classNames(styles.chatTitleCard, {
+                    [styles.chatTitleCardSelected]: x.id === conversationId,
+                  })}
+                >
+                  <Flex justify="space-between" align="center">
+                    <div>{x.name}</div>
+                    {conversationActivated === x.id && x.id !== '' && (
+                      <section>
+                        <Dropdown
+                          menu={{ items: buildConversationItems(x.id) }}
+                        >
+                          <ChatAppCube
+                            className={styles.cubeIcon}
+                          ></ChatAppCube>
+                        </Dropdown>
+                      </section>
+                    )}
+                  </Flex>
+                </Card>
+              ))}
+            </Spin>
           </Flex>
         </Flex>
       </Flex>
diff --git a/web/src/pages/knowledge/index.tsx b/web/src/pages/knowledge/index.tsx
index 78f2af5..8b61e0c 100644
--- a/web/src/pages/knowledge/index.tsx
+++ b/web/src/pages/knowledge/index.tsx
@@ -1,16 +1,16 @@
 import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
 import ModalManager from '@/components/modal-manager';
+import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
+import { useSelectUserInfo } from '@/hooks/userSettingHook';
 import { PlusOutlined } from '@ant-design/icons';
-import { Button, Empty, Flex, Space } from 'antd';
+import { Button, Empty, Flex, Space, Spin } from 'antd';
 import KnowledgeCard from './knowledge-card';
 import KnowledgeCreatingModal from './knowledge-creating-modal';
 
-import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
-import { useSelectUserInfo } from '@/hooks/userSettingHook';
 import styles from './index.less';
 
 const Knowledge = () => {
-  const list = useFetchKnowledgeList();
+  const { list, loading } = useFetchKnowledgeList();
   const userInfo = useSelectUserInfo();
 
   return (
@@ -50,15 +50,23 @@ const Knowledge = () => {
           </ModalManager>
         </Space>
       </div>
-      <Flex gap={'large'} wrap="wrap" className={styles.knowledgeCardContainer}>
-        {list.length > 0 ? (
-          list.map((item: any) => {
-            return <KnowledgeCard item={item} key={item.name}></KnowledgeCard>;
-          })
-        ) : (
-          <Empty></Empty>
-        )}
-      </Flex>
+      <Spin spinning={loading}>
+        <Flex
+          gap={'large'}
+          wrap="wrap"
+          className={styles.knowledgeCardContainer}
+        >
+          {list.length > 0 ? (
+            list.map((item: any) => {
+              return (
+                <KnowledgeCard item={item} key={item.name}></KnowledgeCard>
+              );
+            })
+          ) : (
+            <Empty></Empty>
+          )}
+        </Flex>
+      </Spin>
     </Flex>
   );
 };
diff --git a/web/src/pages/user-setting/hooks.ts b/web/src/pages/user-setting/hooks.ts
index ee670c6..bb770a4 100644
--- a/web/src/pages/user-setting/hooks.ts
+++ b/web/src/pages/user-setting/hooks.ts
@@ -19,5 +19,8 @@ export const useValidateSubmittable = () => {
   return { submittable, form };
 };
 
-export const useGetUserInfoLoading = () =>
+export const useSelectSubmitUserInfoLoading = () =>
   useOneNamespaceEffectsLoading('settingModel', ['setting']);
+
+export const useSelectUserInfoLoading = () =>
+  useOneNamespaceEffectsLoading('settingModel', ['getUserInfo']);
diff --git a/web/src/pages/user-setting/setting-model/hooks.ts b/web/src/pages/user-setting/setting-model/hooks.ts
index 0049cc6..af400bb 100644
--- a/web/src/pages/user-setting/setting-model/hooks.ts
+++ b/web/src/pages/user-setting/setting-model/hooks.ts
@@ -113,3 +113,12 @@ export const useFetchSystemModelSettingOnMount = (visible: boolean) => {
 
   return { systemSetting, allOptions };
 };
+
+export const useSelectModelProvidersLoading = () => {
+  const loading = useOneNamespaceEffectsLoading('settingModel', [
+    'my_llm',
+    'factories_list',
+  ]);
+
+  return loading;
+};
diff --git a/web/src/pages/user-setting/setting-model/index.tsx b/web/src/pages/user-setting/setting-model/index.tsx
index 57cf93e..67b39a8 100644
--- a/web/src/pages/user-setting/setting-model/index.tsx
+++ b/web/src/pages/user-setting/setting-model/index.tsx
@@ -23,12 +23,17 @@ import {
   List,
   Row,
   Space,
+  Spin,
   Typography,
 } from 'antd';
 import { useCallback } from 'react';
 import SettingTitle from '../components/setting-title';
 import ApiKeyModal from './api-key-modal';
-import { useSubmitApiKey, useSubmitSystemModelSetting } from './hooks';
+import {
+  useSelectModelProvidersLoading,
+  useSubmitApiKey,
+  useSubmitSystemModelSetting,
+} from './hooks';
 import SystemModelSettingModal from './system-model-setting-modal';
 
 import styles from './index.less';
@@ -111,6 +116,7 @@ const ModelCard = ({ item, clickApiKey }: IModelCardProps) => {
 const UserSettingModel = () => {
   const factoryList = useFetchLlmFactoryListOnMount();
   const llmList = useFetchMyLlmListOnMount();
+  const loading = useSelectModelProvidersLoading();
   const {
     saveApiKeyLoading,
     initialApiKey,
@@ -191,16 +197,18 @@ const UserSettingModel = () => {
 
   return (
     <>
-      <section className={styles.modelWrapper}>
-        <SettingTitle
-          title="Model Setting"
-          description="Manage your account settings and preferences here."
-          showRightButton
-          clickButton={showSystemSettingModal}
-        ></SettingTitle>
-        <Divider></Divider>
-        <Collapse defaultActiveKey={['1']} ghost items={items} />
-      </section>
+      <Spin spinning={loading}>
+        <section className={styles.modelWrapper}>
+          <SettingTitle
+            title="Model Setting"
+            description="Manage your account settings and preferences here."
+            showRightButton
+            clickButton={showSystemSettingModal}
+          ></SettingTitle>
+          <Divider></Divider>
+          <Collapse defaultActiveKey={['1']} ghost items={items} />
+        </section>
+      </Spin>
       <ApiKeyModal
         visible={apiKeyVisible}
         hideModal={hideApiKeyModal}
diff --git a/web/src/pages/user-setting/setting-profile/index.tsx b/web/src/pages/user-setting/setting-profile/index.tsx
index daf9212..64f48a2 100644
--- a/web/src/pages/user-setting/setting-profile/index.tsx
+++ b/web/src/pages/user-setting/setting-profile/index.tsx
@@ -1,4 +1,8 @@
-import { useSaveSetting, useSelectUserInfo } from '@/hooks/userSettingHook';
+import {
+  useFetchUserInfo,
+  useSaveSetting,
+  useSelectUserInfo,
+} from '@/hooks/userSettingHook';
 import {
   getBase64FromUploadFileList,
   getUploadFileListFromBase64,
@@ -12,6 +16,7 @@ import {
   Input,
   Select,
   Space,
+  Spin,
   Tooltip,
   Upload,
   UploadFile,
@@ -19,7 +24,11 @@ import {
 import { useEffect } from 'react';
 import SettingTitle from '../components/setting-title';
 import { TimezoneList } from '../constants';
-import { useGetUserInfoLoading, useValidateSubmittable } from '../hooks';
+import {
+  useSelectSubmitUserInfoLoading,
+  useSelectUserInfoLoading,
+  useValidateSubmittable,
+} from '../hooks';
 
 import parentStyles from '../index.less';
 import styles from './index.less';
@@ -42,8 +51,10 @@ const tailLayout = {
 const UserSettingProfile = () => {
   const userInfo = useSelectUserInfo();
   const saveSetting = useSaveSetting();
-  const loading = useGetUserInfoLoading();
+  const submitLoading = useSelectSubmitUserInfoLoading();
   const { form, submittable } = useValidateSubmittable();
+  const loading = useSelectUserInfoLoading();
+  useFetchUserInfo();
 
   const onFinish = async (values: any) => {
     const avatar = await getBase64FromUploadFileList(values.avatar);
@@ -66,131 +77,133 @@ const UserSettingProfile = () => {
         description="Update your photo and personal details here."
       ></SettingTitle>
       <Divider />
-      <Form
-        colon={false}
-        name="basic"
-        labelAlign={'left'}
-        labelCol={{ span: 8 }}
-        wrapperCol={{ span: 16 }}
-        style={{ width: '100%' }}
-        initialValues={{ remember: true }}
-        onFinish={onFinish}
-        onFinishFailed={onFinishFailed}
-        form={form}
-        autoComplete="off"
-      >
-        <Form.Item<FieldType>
-          label="Username"
-          name="nickname"
-          rules={[
-            {
-              required: true,
-              message: 'Please input your username!',
-              whitespace: true,
-            },
-          ]}
-        >
-          <Input />
-        </Form.Item>
-        <Divider />
-        <Form.Item<FieldType>
-          label={
-            <div>
-              <Space>
-                Your photo
-                <Tooltip title="prompt text">
-                  <QuestionCircleOutlined />
-                </Tooltip>
-              </Space>
-              <div>This will be displayed on your profile.</div>
-            </div>
-          }
-          name="avatar"
-          valuePropName="fileList"
-          getValueFromEvent={normFile}
+      <Spin spinning={loading}>
+        <Form
+          colon={false}
+          name="basic"
+          labelAlign={'left'}
+          labelCol={{ span: 8 }}
+          wrapperCol={{ span: 16 }}
+          style={{ width: '100%' }}
+          initialValues={{ remember: true }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          form={form}
+          autoComplete="off"
         >
-          <Upload
-            listType="picture-card"
-            maxCount={1}
-            accept="image/*"
-            beforeUpload={() => {
-              return false;
-            }}
-            showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
+          <Form.Item<FieldType>
+            label="Username"
+            name="nickname"
+            rules={[
+              {
+                required: true,
+                message: 'Please input your username!',
+                whitespace: true,
+              },
+            ]}
           >
-            <button style={{ border: 0, background: 'none' }} type="button">
-              <PlusOutlined />
-              <div style={{ marginTop: 8 }}>Upload</div>
-            </button>
-          </Upload>
-        </Form.Item>
-        <Divider />
-        <Form.Item<FieldType>
-          label="Color schema"
-          name="color_schema"
-          rules={[
-            { required: true, message: 'Please select your color schema!' },
-          ]}
-        >
-          <Select placeholder="select your color schema">
-            <Option value="Bright">Bright</Option>
-            <Option value="Dark">Dark</Option>
-          </Select>
-        </Form.Item>
-        <Divider />
-        <Form.Item<FieldType>
-          label="Language"
-          name="language"
-          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>
-        <Divider />
-        <Form.Item<FieldType>
-          label="Timezone"
-          name="timezone"
-          rules={[{ required: true, message: 'Please input your timezone!' }]}
-        >
-          <Select placeholder="select your timezone" showSearch>
-            {TimezoneList.map((x) => (
-              <Option value={x} key={x}>
-                {x}
-              </Option>
-            ))}
-          </Select>
-        </Form.Item>
-        <Divider />
-        <Form.Item label="Email address">
-          <Form.Item<FieldType> name="email" noStyle>
-            <Input disabled />
+            <Input />
           </Form.Item>
-          <p className={parentStyles.itemDescription}>
-            Once registered, an account cannot be changed and can only be
-            cancelled.
-          </p>
-        </Form.Item>
-        <Form.Item
-          {...tailLayout}
-          shouldUpdate={(prevValues, curValues) =>
-            prevValues.additional !== curValues.additional
-          }
-        >
-          <Space>
-            <Button htmlType="button">Cancel</Button>
-            <Button
-              type="primary"
-              htmlType="submit"
-              disabled={!submittable}
-              loading={loading}
+          <Divider />
+          <Form.Item<FieldType>
+            label={
+              <div>
+                <Space>
+                  Your photo
+                  <Tooltip title="prompt text">
+                    <QuestionCircleOutlined />
+                  </Tooltip>
+                </Space>
+                <div>This will be displayed on your profile.</div>
+              </div>
+            }
+            name="avatar"
+            valuePropName="fileList"
+            getValueFromEvent={normFile}
+          >
+            <Upload
+              listType="picture-card"
+              maxCount={1}
+              accept="image/*"
+              beforeUpload={() => {
+                return false;
+              }}
+              showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
             >
-              Save
-            </Button>
-          </Space>
-        </Form.Item>
-      </Form>
+              <button style={{ border: 0, background: 'none' }} type="button">
+                <PlusOutlined />
+                <div style={{ marginTop: 8 }}>Upload</div>
+              </button>
+            </Upload>
+          </Form.Item>
+          <Divider />
+          <Form.Item<FieldType>
+            label="Color schema"
+            name="color_schema"
+            rules={[
+              { required: true, message: 'Please select your color schema!' },
+            ]}
+          >
+            <Select placeholder="select your color schema">
+              <Option value="Bright">Bright</Option>
+              <Option value="Dark">Dark</Option>
+            </Select>
+          </Form.Item>
+          <Divider />
+          <Form.Item<FieldType>
+            label="Language"
+            name="language"
+            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>
+          <Divider />
+          <Form.Item<FieldType>
+            label="Timezone"
+            name="timezone"
+            rules={[{ required: true, message: 'Please input your timezone!' }]}
+          >
+            <Select placeholder="select your timezone" showSearch>
+              {TimezoneList.map((x) => (
+                <Option value={x} key={x}>
+                  {x}
+                </Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Divider />
+          <Form.Item label="Email address">
+            <Form.Item<FieldType> name="email" noStyle>
+              <Input disabled />
+            </Form.Item>
+            <p className={parentStyles.itemDescription}>
+              Once registered, an account cannot be changed and can only be
+              cancelled.
+            </p>
+          </Form.Item>
+          <Form.Item
+            {...tailLayout}
+            shouldUpdate={(prevValues, curValues) =>
+              prevValues.additional !== curValues.additional
+            }
+          >
+            <Space>
+              <Button htmlType="button">Cancel</Button>
+              <Button
+                type="primary"
+                htmlType="submit"
+                disabled={!submittable}
+                loading={submitLoading}
+              >
+                Save
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </Spin>
     </section>
   );
 };
diff --git a/web/src/utils/fileUtil.ts b/web/src/utils/fileUtil.ts
index 44e43c6..40b6bd9 100644
--- a/web/src/utils/fileUtil.ts
+++ b/web/src/utils/fileUtil.ts
@@ -48,8 +48,14 @@ export const getUploadFileListFromBase64 = (avatar: string) => {
 
 export const getBase64FromUploadFileList = async (fileList?: UploadFile[]) => {
   if (Array.isArray(fileList) && fileList.length > 0) {
-    const base64 = await transformFile2Base64(fileList[0].originFileObj);
-    return base64;
+    const file = fileList[0];
+    const originFileObj = file.originFileObj;
+    if (originFileObj) {
+      const base64 = await transformFile2Base64(originFileObj);
+      return base64;
+    } else {
+      return file.thumbUrl;
+    }
     // return fileList[0].thumbUrl; TODO: Even JPG files will be converted to base64 parameters in png format
   }
 
-- 
GitLab