balibabu commited on
Commit
3054c20
·
1 Parent(s): 64a0633

feat: locate the specific location of the document based on the coordinates of the chunk and add Upload to AssistantSetting (#92)

Browse files

* feat: add Upload to AssistantSetting

* feat: locate the specific location of the document based on the coordinates of the chunk

web/src/interfaces/database/knowledge.ts CHANGED
@@ -74,6 +74,7 @@ export interface IChunk {
74
  docnm_kwd: string;
75
  img_id: string;
76
  important_kwd: any[];
 
77
  }
78
 
79
  export interface ITestingChunk {
 
74
  docnm_kwd: string;
75
  img_id: string;
76
  important_kwd: any[];
77
+ positions: number[][];
78
  }
79
 
80
  export interface ITestingChunk {
web/src/less/variable.less CHANGED
@@ -8,6 +8,8 @@
8
  @gray11: rgba(232, 232, 234, 1);
9
  @purple: rgba(127, 86, 217, 1);
10
  @selectedBackgroundColor: rgba(239, 248, 255, 1);
 
 
11
 
12
  @fontSize12: 12px;
13
  @fontSize14: 14px;
 
8
  @gray11: rgba(232, 232, 234, 1);
9
  @purple: rgba(127, 86, 217, 1);
10
  @selectedBackgroundColor: rgba(239, 248, 255, 1);
11
+ @blurBackground: rgba(22, 119, 255, 0.5);
12
+ @blurBackgroundHover: rgba(22, 119, 255, 0.2);
13
 
14
  @fontSize12: 12px;
15
  @fontSize14: 14px;
web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less CHANGED
@@ -13,6 +13,28 @@
13
  color: red;
14
  font-style: normal;
15
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
18
  .cardSelected {
 
13
  color: red;
14
  font-style: normal;
15
  }
16
+
17
+ caption {
18
+ color: @blurBackground;
19
+ font-size: 20px;
20
+ height: 50px;
21
+ line-height: 50px;
22
+ font-weight: 600;
23
+ margin-bottom: 10px;
24
+ }
25
+
26
+ th {
27
+ color: #fff;
28
+ background-color: @blurBackground;
29
+ }
30
+
31
+ td:hover {
32
+ background: @blurBackgroundHover;
33
+ }
34
+
35
+ tr:nth-child(even) {
36
+ background-color: #f2f2f2;
37
+ }
38
  }
39
 
40
  .cardSelected {
web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx CHANGED
@@ -64,9 +64,7 @@ const ChunkCard = ({
64
  onClick={handleContentClick}
65
  className={styles.content}
66
  dangerouslySetInnerHTML={{ __html: item.content_with_weight }}
67
- >
68
- {/* {item.content_with_weight} */}
69
- </section>
70
  <div>
71
  <Switch checked={enabled} onChange={onChange} />
72
  </div>
 
64
  onClick={handleContentClick}
65
  className={styles.content}
66
  dangerouslySetInnerHTML={{ __html: item.content_with_weight }}
67
+ ></section>
 
 
68
  <div>
69
  <Switch checked={enabled} onChange={onChange} />
70
  </div>
web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/hightlights.ts CHANGED
@@ -6,21 +6,27 @@ export const testHighlights = [
6
  position: {
7
  boundingRect: {
8
  x1: 219.7,
 
9
  y1: 204.3,
 
10
  x2: 547.0,
 
11
  y2: 264.0,
12
- width: 849,
13
- height: 1200,
14
  },
15
  rects: [
16
- {
17
- x1: 219.7,
18
- y1: 204.3,
19
- x2: 547.0,
20
- y2: 264.0,
21
- width: 849,
22
- height: 1200,
23
- },
 
 
 
 
24
  ],
25
  pageNumber: 9,
26
  },
@@ -28,6 +34,56 @@ export const testHighlights = [
28
  text: 'Flow or TypeScript?',
29
  emoji: '🔥',
30
  },
31
- id: '8245652131754351',
32
  },
33
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  position: {
7
  boundingRect: {
8
  x1: 219.7,
9
+ // x1: 419.7,
10
  y1: 204.3,
11
+ // y1: 304.3,
12
  x2: 547.0,
13
+ // x2: 747.0,
14
  y2: 264.0,
15
+ // y2: 364.0,
 
16
  },
17
  rects: [
18
+ // {
19
+ // x1: 219.7,
20
+ // // x1: 419.7,
21
+ // y1: 204.3,
22
+ // // y1: 304.3,
23
+ // x2: 547.0,
24
+ // // x2: 747.0,
25
+ // y2: 264.0,
26
+ // // y2: 364.0,
27
+ // width: 849,
28
+ // height: 1200,
29
+ // },
30
  ],
31
  pageNumber: 9,
32
  },
 
34
  text: 'Flow or TypeScript?',
35
  emoji: '🔥',
36
  },
37
+ id: 'jsdlihdkghergjl',
38
  },
39
+ {
40
+ content: {
41
+ text: '图2:乘联会预计6 月新能源乘用车厂商批发销量74 万辆,环比增长10%,同比增长30%。',
42
+ },
43
+ position: {
44
+ boundingRect: {
45
+ x1: 219.0,
46
+ x2: 546.0,
47
+ y1: 616.0,
48
+ y2: 674.7,
49
+ },
50
+ rects: [],
51
+ pageNumber: 6,
52
+ },
53
+ comment: {
54
+ text: 'Flow or TypeScript?',
55
+ emoji: '🔥',
56
+ },
57
+ id: 'bfdbtymkhjildbfghserrgrt',
58
+ },
59
+ {
60
+ content: {
61
+ text: '图2:乘联会预计6 月新能源乘用车厂商批发销量74 万辆,环比增长10%,同比增长30%。',
62
+ },
63
+ position: {
64
+ boundingRect: {
65
+ x1: 73.7,
66
+ x2: 391.7,
67
+ y1: 570.3,
68
+ y2: 676.3,
69
+ },
70
+ rects: [],
71
+ pageNumber: 1,
72
+ },
73
+ comment: {
74
+ text: '',
75
+ emoji: '',
76
+ },
77
+ id: 'fgnhxdvsesgmghyu',
78
+ },
79
+ ].map((x) => {
80
+ const boundingRect = x.position.boundingRect;
81
+ const ret: any = {
82
+ width: 849,
83
+ height: 1200,
84
+ };
85
+ Object.entries(boundingRect).forEach(([key, value]) => {
86
+ ret[key] = value / 0.7;
87
+ });
88
+ return { ...x, position: { ...x.position, boundingRect: ret, rects: [ret] } };
89
+ });
web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/index.less CHANGED
@@ -6,6 +6,9 @@
6
  position: relative;
7
  :global(.PdfHighlighter) {
8
  overflow-x: hidden;
9
- // left: 0;
 
 
 
10
  }
11
  }
 
6
  position: relative;
7
  :global(.PdfHighlighter) {
8
  overflow-x: hidden;
9
+ }
10
+ :global(.Highlight--scrolledTo .Highlight__part) {
11
+ overflow-x: hidden;
12
+ background-color: rgba(255, 226, 143, 1);
13
  }
14
  }
web/src/pages/add-knowledge/components/knowledge-chunk/components/document-preview/preview.tsx CHANGED
@@ -1,16 +1,15 @@
1
  import { Spin } from 'antd';
2
- import { useRef, useState } from 'react';
3
- import type { NewHighlight } from 'react-pdf-highlighter';
4
  import {
5
  AreaHighlight,
6
  Highlight,
 
7
  PdfHighlighter,
8
  PdfLoader,
9
  Popup,
10
  Tip,
11
  } from 'react-pdf-highlighter';
12
- import { useGetSelectedChunk } from '../../hooks';
13
- import { testHighlights } from './hightlights';
14
  import { useGetDocumentUrl } from './hooks';
15
 
16
  import styles from './index.less';
@@ -36,7 +35,9 @@ const Preview = ({ selectedChunkId }: IProps) => {
36
  const url = useGetDocumentUrl();
37
  const selectedChunk = useGetSelectedChunk(selectedChunkId);
38
 
39
- const [state, setState] = useState<any>(testHighlights);
 
 
40
  const ref = useRef((highlight: any) => {});
41
 
42
  const parseIdFromHash = () =>
@@ -67,7 +68,7 @@ const Preview = ({ selectedChunkId }: IProps) => {
67
 
68
  console.log('Saving highlight', highlight);
69
 
70
- setState([{ ...highlight, id: getNextId() }, ...highlights]);
71
  };
72
 
73
  const updateHighlight = (
@@ -77,29 +78,31 @@ const Preview = ({ selectedChunkId }: IProps) => {
77
  ) => {
78
  console.log('Updating highlight', highlightId, position, content);
79
 
80
- setState(
81
- state.map((h: any) => {
82
- const {
83
- id,
84
- position: originalPosition,
85
- content: originalContent,
86
- ...rest
87
- } = h;
88
- return id === highlightId
89
- ? {
90
- id,
91
- position: { ...originalPosition, ...position },
92
- content: { ...originalContent, ...content },
93
- ...rest,
94
- }
95
- : h;
96
- }),
97
- );
98
  };
99
 
100
- // useEffect(() => {
101
- // ref.current(testHighlights[0]);
102
- // }, [selectedChunk]);
 
 
103
 
104
  return (
105
  <div className={styles.documentContainer}>
 
1
  import { Spin } from 'antd';
2
+ import { useEffect, useRef } from 'react';
 
3
  import {
4
  AreaHighlight,
5
  Highlight,
6
+ NewHighlight,
7
  PdfHighlighter,
8
  PdfLoader,
9
  Popup,
10
  Tip,
11
  } from 'react-pdf-highlighter';
12
+ import { useGetChunkHighlights, useGetSelectedChunk } from '../../hooks';
 
13
  import { useGetDocumentUrl } from './hooks';
14
 
15
  import styles from './index.less';
 
35
  const url = useGetDocumentUrl();
36
  const selectedChunk = useGetSelectedChunk(selectedChunkId);
37
 
38
+ // const [state, setState] = useState<any>(testHighlights);
39
+ const state = useGetChunkHighlights(selectedChunkId);
40
+
41
  const ref = useRef((highlight: any) => {});
42
 
43
  const parseIdFromHash = () =>
 
68
 
69
  console.log('Saving highlight', highlight);
70
 
71
+ // setState([{ ...highlight, id: getNextId() }, ...highlights]);
72
  };
73
 
74
  const updateHighlight = (
 
78
  ) => {
79
  console.log('Updating highlight', highlightId, position, content);
80
 
81
+ // setState(
82
+ // state.map((h: any) => {
83
+ // const {
84
+ // id,
85
+ // position: originalPosition,
86
+ // content: originalContent,
87
+ // ...rest
88
+ // } = h;
89
+ // return id === highlightId
90
+ // ? {
91
+ // id,
92
+ // position: { ...originalPosition, ...position },
93
+ // content: { ...originalContent, ...content },
94
+ // ...rest,
95
+ // }
96
+ // : h;
97
+ // }),
98
+ // );
99
  };
100
 
101
+ useEffect(() => {
102
+ if (state.length > 0) {
103
+ ref.current(state[0]);
104
+ }
105
+ }, [state]);
106
 
107
  return (
108
  <div className={styles.documentContainer}>
web/src/pages/add-knowledge/components/knowledge-chunk/hooks.ts CHANGED
@@ -1,6 +1,8 @@
1
  import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
2
- import { useCallback, useState } from 'react';
 
3
  import { useSelector } from 'umi';
 
4
 
5
  export const useSelectDocumentInfo = () => {
6
  const documentInfo: IKnowledgeFile = useSelector(
@@ -28,5 +30,46 @@ export const useHandleChunkCardClick = () => {
28
 
29
  export const useGetSelectedChunk = (selectedChunkId: string) => {
30
  const chunkList: IChunk[] = useSelectChunkList();
31
- return chunkList.find((x) => x.chunk_id === selectedChunkId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  };
 
1
  import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
2
+ import { useCallback, useMemo, useState } from 'react';
3
+ import { IHighlight } from 'react-pdf-highlighter';
4
  import { useSelector } from 'umi';
5
+ import { v4 as uuid } from 'uuid';
6
 
7
  export const useSelectDocumentInfo = () => {
8
  const documentInfo: IKnowledgeFile = useSelector(
 
30
 
31
  export const useGetSelectedChunk = (selectedChunkId: string) => {
32
  const chunkList: IChunk[] = useSelectChunkList();
33
+ return (
34
+ chunkList.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk)
35
+ );
36
+ };
37
+
38
+ export const useGetChunkHighlights = (
39
+ selectedChunkId: string,
40
+ ): IHighlight[] => {
41
+ const selectedChunk: IChunk = useGetSelectedChunk(selectedChunkId);
42
+
43
+ const highlights: IHighlight[] = useMemo(() => {
44
+ return Array.isArray(selectedChunk?.positions)
45
+ ? selectedChunk?.positions?.map((x) => {
46
+ const actualPositions = x.map((y, index) =>
47
+ index !== 0 ? y / 0.7 : y,
48
+ );
49
+ const boundingRect = {
50
+ width: 849,
51
+ height: 1200,
52
+ x1: actualPositions[1],
53
+ x2: actualPositions[2],
54
+ y1: actualPositions[3],
55
+ y2: actualPositions[4],
56
+ };
57
+ return {
58
+ id: uuid(),
59
+ comment: {
60
+ text: '',
61
+ emoji: '',
62
+ },
63
+ content: { text: selectedChunk.content_with_weight },
64
+ position: {
65
+ boundingRect: boundingRect,
66
+ rects: [boundingRect],
67
+ pageNumber: x[0],
68
+ },
69
+ };
70
+ })
71
+ : [];
72
+ }, [selectedChunk]);
73
+
74
+ return highlights;
75
  };
web/src/pages/add-knowledge/components/knowledge-file/index.tsx CHANGED
@@ -23,7 +23,7 @@ import {
23
  import type { ColumnsType } from 'antd/es/table';
24
  import { PaginationProps } from 'antd/lib';
25
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
26
- import { Link, useDispatch, useNavigate, useSelector } from 'umi';
27
  import CreateEPModal from './createEFileModal';
28
  import styles from './index.less';
29
  import ParsingActionCell from './parsing-action-cell';
@@ -144,19 +144,22 @@ const KnowledgeFile = () => {
144
  });
145
  }, [dispatch]);
146
 
 
 
 
 
147
  const actionItems: MenuProps['items'] = useMemo(() => {
148
  return [
149
  {
150
  key: '1',
 
151
  label: (
152
  <div>
153
  <Button type="link">
154
- <Link to={`/knowledge/dataset/upload?id=${knowledgeBaseId}`}>
155
- <Space>
156
- <FileTextOutlined />
157
- Local files
158
- </Space>
159
- </Link>
160
  </Button>
161
  </div>
162
  ),
@@ -164,9 +167,10 @@ const KnowledgeFile = () => {
164
  { type: 'divider' },
165
  {
166
  key: '2',
 
167
  label: (
168
  <div>
169
- <Button type="link" onClick={showCEFModal}>
170
  <FileOutlined />
171
  Create empty file
172
  </Button>
@@ -175,7 +179,7 @@ const KnowledgeFile = () => {
175
  // disabled: true,
176
  },
177
  ];
178
- }, [knowledgeBaseId, showCEFModal]);
179
 
180
  const toChunk = (id: string) => {
181
  navigate(
 
23
  import type { ColumnsType } from 'antd/es/table';
24
  import { PaginationProps } from 'antd/lib';
25
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
26
+ import { useDispatch, useNavigate, useSelector } from 'umi';
27
  import CreateEPModal from './createEFileModal';
28
  import styles from './index.less';
29
  import ParsingActionCell from './parsing-action-cell';
 
144
  });
145
  }, [dispatch]);
146
 
147
+ const linkToUploadPage = useCallback(() => {
148
+ navigate(`/knowledge/dataset/upload?id=${knowledgeBaseId}`);
149
+ }, [navigate, knowledgeBaseId]);
150
+
151
  const actionItems: MenuProps['items'] = useMemo(() => {
152
  return [
153
  {
154
  key: '1',
155
+ onClick: linkToUploadPage,
156
  label: (
157
  <div>
158
  <Button type="link">
159
+ <Space>
160
+ <FileTextOutlined />
161
+ Local files
162
+ </Space>
 
 
163
  </Button>
164
  </div>
165
  ),
 
167
  { type: 'divider' },
168
  {
169
  key: '2',
170
+ onClick: showCEFModal,
171
  label: (
172
  <div>
173
+ <Button type="link">
174
  <FileOutlined />
175
  Create empty file
176
  </Button>
 
179
  // disabled: true,
180
  },
181
  ];
182
+ }, [linkToUploadPage, showCEFModal]);
183
 
184
  const toChunk = (id: string) => {
185
  navigate(
web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx CHANGED
@@ -1,9 +1,10 @@
1
- import { Form, Input, Select } from 'antd';
2
 
3
  import classNames from 'classnames';
4
  import { ISegmentedContentProps } from '../interface';
5
 
6
  import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
 
7
  import styles from './index.less';
8
 
9
  const AssistantSetting = ({ show }: ISegmentedContentProps) => {
@@ -13,6 +14,13 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
13
  value: x.id,
14
  }));
15
 
 
 
 
 
 
 
 
16
  return (
17
  <section
18
  className={classNames({
@@ -26,8 +34,22 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
26
  >
27
  <Input placeholder="e.g. Resume Jarvis" />
28
  </Form.Item>
29
- <Form.Item name={'icon'} label="Assistant avatar">
30
- <Input />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </Form.Item>
32
  <Form.Item name={'language'} label="Language" initialValue={'Chinese'}>
33
  <Select
 
1
+ import { Form, Input, Select, Upload } from 'antd';
2
 
3
  import classNames from 'classnames';
4
  import { ISegmentedContentProps } from '../interface';
5
 
6
  import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
7
+ import { PlusOutlined } from '@ant-design/icons';
8
  import styles from './index.less';
9
 
10
  const AssistantSetting = ({ show }: ISegmentedContentProps) => {
 
14
  value: x.id,
15
  }));
16
 
17
+ const normFile = (e: any) => {
18
+ if (Array.isArray(e)) {
19
+ return e;
20
+ }
21
+ return e?.fileList;
22
+ };
23
+
24
  return (
25
  <section
26
  className={classNames({
 
34
  >
35
  <Input placeholder="e.g. Resume Jarvis" />
36
  </Form.Item>
37
+ <Form.Item
38
+ name="icon"
39
+ label="Assistant avatar"
40
+ valuePropName="fileList"
41
+ getValueFromEvent={normFile}
42
+ >
43
+ <Upload
44
+ listType="picture-card"
45
+ maxCount={1}
46
+ showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }}
47
+ >
48
+ <button style={{ border: 0, background: 'none' }} type="button">
49
+ <PlusOutlined />
50
+ <div style={{ marginTop: 8 }}>Upload</div>
51
+ </button>
52
+ </Upload>
53
  </Form.Item>
54
  <Form.Item name={'language'} label="Language" initialValue={'Chinese'}>
55
  <Select
web/src/pages/chat/chat-configuration-modal/index.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { ReactComponent as ChatConfigurationAtom } from '@/assets/svg/chat-configuration-atom.svg';
2
  import { IModalManagerChildrenProps } from '@/components/modal-manager';
3
- import { Divider, Flex, Form, Modal, Segmented } from 'antd';
4
  import { SegmentedValue } from 'antd/es/segmented';
5
  import omit from 'lodash/omit';
6
  import { useEffect, useRef, useState } from 'react';
@@ -67,6 +67,14 @@ const ChatConfigurationModal = ({ visible, hideModal, id }: IProps) => {
67
  ...excludeUnEnabledVariables(values),
68
  ]);
69
  const emptyResponse = nextValues.prompt_config?.empty_response ?? '';
 
 
 
 
 
 
 
 
70
  const finalValues = {
71
  dialog_id: id,
72
  ...nextValues,
@@ -75,6 +83,7 @@ const ChatConfigurationModal = ({ visible, hideModal, id }: IProps) => {
75
  parameters: promptEngineRef.current,
76
  empty_response: emptyResponse,
77
  },
 
78
  };
79
  console.info(promptEngineRef.current);
80
  console.info(nextValues);
@@ -112,7 +121,13 @@ const ChatConfigurationModal = ({ visible, hideModal, id }: IProps) => {
112
  );
113
 
114
  useEffect(() => {
115
- form.setFieldsValue(currentDialog);
 
 
 
 
 
 
116
  }, [currentDialog, form]);
117
 
118
  return (
 
1
  import { ReactComponent as ChatConfigurationAtom } from '@/assets/svg/chat-configuration-atom.svg';
2
  import { IModalManagerChildrenProps } from '@/components/modal-manager';
3
+ import { Divider, Flex, Form, Modal, Segmented, UploadFile } from 'antd';
4
  import { SegmentedValue } from 'antd/es/segmented';
5
  import omit from 'lodash/omit';
6
  import { useEffect, useRef, useState } from 'react';
 
67
  ...excludeUnEnabledVariables(values),
68
  ]);
69
  const emptyResponse = nextValues.prompt_config?.empty_response ?? '';
70
+
71
+ const fileList = values.icon;
72
+ let icon;
73
+
74
+ if (Array.isArray(fileList) && fileList.length > 0) {
75
+ icon = fileList[0].thumbUrl;
76
+ }
77
+
78
  const finalValues = {
79
  dialog_id: id,
80
  ...nextValues,
 
83
  parameters: promptEngineRef.current,
84
  empty_response: emptyResponse,
85
  },
86
+ icon,
87
  };
88
  console.info(promptEngineRef.current);
89
  console.info(nextValues);
 
121
  );
122
 
123
  useEffect(() => {
124
+ const icon = currentDialog.icon;
125
+ let fileList: UploadFile[] = [];
126
+
127
+ if (icon) {
128
+ fileList = [{ uid: '1', name: 'file', thumbUrl: icon, status: 'done' }];
129
+ }
130
+ form.setFieldsValue({ ...currentDialog, icon: fileList });
131
  }, [currentDialog, form]);
132
 
133
  return (
web/src/pages/chat/index.tsx CHANGED
@@ -2,6 +2,7 @@ import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg';
2
  import { useSetModalState } from '@/hooks/commonHooks';
3
  import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons';
4
  import {
 
5
  Button,
6
  Card,
7
  Divider,
@@ -208,8 +209,8 @@ const Chat = () => {
208
  onClick={handleDialogCardClick(x.id)}
209
  >
210
  <Flex justify="space-between" align="center">
211
- <Space>
212
- {x.icon}
213
  <section>
214
  <b>{x.name}</b>
215
  <div>{x.description}</div>
 
2
  import { useSetModalState } from '@/hooks/commonHooks';
3
  import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons';
4
  import {
5
+ Avatar,
6
  Button,
7
  Card,
8
  Divider,
 
209
  onClick={handleDialogCardClick(x.id)}
210
  >
211
  <Flex justify="space-between" align="center">
212
+ <Space size={15}>
213
+ <Avatar src={x.icon} shape={'square'} />
214
  <section>
215
  <b>{x.name}</b>
216
  <div>{x.description}</div>