balibabu commited on
Commit
2eeb8b1
·
1 Parent(s): dee924b

feat: Supports chatting with files/images #1880 (#1943)

Browse files

### What problem does this PR solve?

feat: Supports chatting with files/images #1880

### Type of change


- [x] New Feature (non-breaking change which adds functionality)

web/src/{pages/add-knowledge/components/knowledge-chunk/components/knowledge-graph → components/indented-tree}/indented-tree.tsx RENAMED
File without changes
web/src/components/indented-tree/modal.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useFetchKnowledgeGraph } from '@/hooks/chunk-hooks';
2
+ import { Modal } from 'antd';
3
+ import { useTranslation } from 'react-i18next';
4
+ import IndentedTree from './indented-tree';
5
+
6
+ import { IModalProps } from '@/interfaces/common';
7
+
8
+ const IndentedTreeModal = ({
9
+ documentId,
10
+ visible,
11
+ hideModal,
12
+ }: IModalProps<any> & { documentId: string }) => {
13
+ const { data } = useFetchKnowledgeGraph(documentId);
14
+ const { t } = useTranslation();
15
+
16
+ return (
17
+ <Modal
18
+ title={t('chunk.graph')}
19
+ open={visible}
20
+ onCancel={hideModal}
21
+ width={'90vw'}
22
+ footer={null}
23
+ >
24
+ <section>
25
+ <IndentedTree data={data?.data?.mind_map} show></IndentedTree>
26
+ </section>
27
+ </Modal>
28
+ );
29
+ };
30
+
31
+ export default IndentedTreeModal;
web/src/components/message-input/index.tsx ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Authorization } from '@/constants/authorization';
2
+ import { useTranslate } from '@/hooks/common-hooks';
3
+ import { getAuthorization } from '@/utils/authorization-util';
4
+ import { PlusOutlined } from '@ant-design/icons';
5
+ import type { GetProp, UploadFile } from 'antd';
6
+ import { Button, Flex, Input, Upload, UploadProps } from 'antd';
7
+ import get from 'lodash/get';
8
+ import { ChangeEventHandler, useCallback, useState } from 'react';
9
+
10
+ type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
11
+
12
+ interface IProps {
13
+ disabled: boolean;
14
+ value: string;
15
+ sendDisabled: boolean;
16
+ sendLoading: boolean;
17
+ onPressEnter(documentIds: string[]): Promise<any>;
18
+ onInputChange: ChangeEventHandler<HTMLInputElement>;
19
+ conversationId: string;
20
+ }
21
+
22
+ const getBase64 = (file: FileType): Promise<string> =>
23
+ new Promise((resolve, reject) => {
24
+ const reader = new FileReader();
25
+ reader.readAsDataURL(file as any);
26
+ reader.onload = () => resolve(reader.result as string);
27
+ reader.onerror = (error) => reject(error);
28
+ });
29
+
30
+ const MessageInput = ({
31
+ disabled,
32
+ value,
33
+ onPressEnter,
34
+ sendDisabled,
35
+ sendLoading,
36
+ onInputChange,
37
+ conversationId,
38
+ }: IProps) => {
39
+ const { t } = useTranslate('chat');
40
+
41
+ const [fileList, setFileList] = useState<UploadFile[]>([
42
+ // {
43
+ // uid: '-1',
44
+ // name: 'image.png',
45
+ // status: 'done',
46
+ // url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
47
+ // },
48
+ // {
49
+ // uid: '-xxx',
50
+ // percent: 50,
51
+ // name: 'image.png',
52
+ // status: 'uploading',
53
+ // url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
54
+ // },
55
+ // {
56
+ // uid: '-5',
57
+ // name: 'image.png',
58
+ // status: 'error',
59
+ // },
60
+ ]);
61
+
62
+ const handlePreview = async (file: UploadFile) => {
63
+ if (!file.url && !file.preview) {
64
+ file.preview = await getBase64(file.originFileObj as FileType);
65
+ }
66
+
67
+ // setPreviewImage(file.url || (file.preview as string));
68
+ // setPreviewOpen(true);
69
+ };
70
+
71
+ const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
72
+ console.log('🚀 ~ newFileList:', newFileList);
73
+ setFileList(newFileList);
74
+ };
75
+
76
+ const handlePressEnter = useCallback(async () => {
77
+ const ids = fileList.reduce((pre, cur) => {
78
+ return pre.concat(get(cur, 'response.data', []));
79
+ }, []);
80
+
81
+ await onPressEnter(ids);
82
+ setFileList([]);
83
+ }, [fileList, onPressEnter]);
84
+
85
+ const uploadButton = (
86
+ <button style={{ border: 0, background: 'none' }} type="button">
87
+ <PlusOutlined />
88
+ <div style={{ marginTop: 8 }}>Upload</div>
89
+ </button>
90
+ );
91
+
92
+ return (
93
+ <Flex gap={10} vertical>
94
+ <Input
95
+ size="large"
96
+ placeholder={t('sendPlaceholder')}
97
+ value={value}
98
+ disabled={disabled}
99
+ suffix={
100
+ <Button
101
+ type="primary"
102
+ onClick={handlePressEnter}
103
+ loading={sendLoading}
104
+ disabled={sendDisabled}
105
+ >
106
+ {t('send')}
107
+ </Button>
108
+ }
109
+ onPressEnter={handlePressEnter}
110
+ onChange={onInputChange}
111
+ />
112
+ <Upload
113
+ action="/v1/document/upload_and_parse"
114
+ listType="picture-card"
115
+ fileList={fileList}
116
+ onPreview={handlePreview}
117
+ onChange={handleChange}
118
+ multiple
119
+ headers={{ [Authorization]: getAuthorization() }}
120
+ data={{ conversation_id: conversationId }}
121
+ method="post"
122
+ >
123
+ {fileList.length >= 8 ? null : uploadButton}
124
+ </Upload>
125
+ </Flex>
126
+ );
127
+ };
128
+
129
+ export default MessageInput;
web/src/components/message-item/index.tsx CHANGED
@@ -1,15 +1,17 @@
1
  import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
2
  import { MessageType } from '@/constants/chat';
3
- import { useTranslate } from '@/hooks/common-hooks';
4
  import { useSelectFileThumbnails } from '@/hooks/knowledge-hooks';
5
  import { IReference, Message } from '@/interfaces/database/chat';
6
  import { IChunk } from '@/interfaces/database/knowledge';
7
  import classNames from 'classnames';
8
- import { useMemo } from 'react';
9
 
 
10
  import MarkdownContent from '@/pages/chat/markdown-content';
11
- import { getExtension } from '@/utils/document-util';
12
- import { Avatar, Flex, List } from 'antd';
 
13
  import NewDocumentLink from '../new-document-link';
14
  import SvgIcon from '../svg-icon';
15
  import styles from './index.less';
@@ -32,8 +34,13 @@ const MessageItem = ({
32
  clickDocumentButton,
33
  }: IProps) => {
34
  const isAssistant = item.role === MessageType.Assistant;
 
35
  const { t } = useTranslate('chat');
36
  const fileThumbnails = useSelectFileThumbnails();
 
 
 
 
37
 
38
  const referenceDocumentList = useMemo(() => {
39
  return reference?.doc_aggs ?? [];
@@ -47,6 +54,21 @@ const MessageItem = ({
47
  return loading ? text?.concat('~~2$$') : text;
48
  }, [item.content, loading, t]);
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  return (
51
  <div
52
  className={classNames(styles.messageItem, {
@@ -124,11 +146,62 @@ const MessageItem = ({
124
  }}
125
  />
126
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  </Flex>
128
  </div>
129
  </section>
 
 
 
 
 
 
 
130
  </div>
131
  );
132
  };
133
 
134
- export default MessageItem;
 
1
  import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
2
  import { MessageType } from '@/constants/chat';
3
+ import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
4
  import { useSelectFileThumbnails } from '@/hooks/knowledge-hooks';
5
  import { IReference, Message } from '@/interfaces/database/chat';
6
  import { IChunk } from '@/interfaces/database/knowledge';
7
  import classNames from 'classnames';
8
+ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
9
 
10
+ import { useFetchDocumentInfosByIds } from '@/hooks/document-hooks';
11
  import MarkdownContent from '@/pages/chat/markdown-content';
12
+ import { getExtension, isImage } from '@/utils/document-util';
13
+ import { Avatar, Button, Flex, List } from 'antd';
14
+ import IndentedTreeModal from '../indented-tree/modal';
15
  import NewDocumentLink from '../new-document-link';
16
  import SvgIcon from '../svg-icon';
17
  import styles from './index.less';
 
34
  clickDocumentButton,
35
  }: IProps) => {
36
  const isAssistant = item.role === MessageType.Assistant;
37
+ const isUser = item.role === MessageType.User;
38
  const { t } = useTranslate('chat');
39
  const fileThumbnails = useSelectFileThumbnails();
40
+ const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds();
41
+ console.log('🚀 ~ documentList:', documentList);
42
+ const { visible, hideModal, showModal } = useSetModalState();
43
+ const [clickedDocumentId, setClickedDocumentId] = useState('');
44
 
45
  const referenceDocumentList = useMemo(() => {
46
  return reference?.doc_aggs ?? [];
 
54
  return loading ? text?.concat('~~2$$') : text;
55
  }, [item.content, loading, t]);
56
 
57
+ const handleUserDocumentClick = useCallback(
58
+ (id: string) => () => {
59
+ setClickedDocumentId(id);
60
+ showModal();
61
+ },
62
+ [showModal],
63
+ );
64
+
65
+ useEffect(() => {
66
+ const ids = item?.doc_ids ?? [];
67
+ if (ids.length) {
68
+ setDocumentIds(ids);
69
+ }
70
+ }, [item.doc_ids, setDocumentIds]);
71
+
72
  return (
73
  <div
74
  className={classNames(styles.messageItem, {
 
146
  }}
147
  />
148
  )}
149
+ {isUser && documentList.length > 0 && (
150
+ <List
151
+ bordered
152
+ dataSource={documentList}
153
+ renderItem={(item) => {
154
+ const fileThumbnail = fileThumbnails[item.id];
155
+ const fileExtension = getExtension(item.name);
156
+ return (
157
+ <List.Item>
158
+ <Flex gap={'small'} align="center">
159
+ {fileThumbnail ? (
160
+ <img
161
+ src={fileThumbnail}
162
+ className={styles.thumbnailImg}
163
+ ></img>
164
+ ) : (
165
+ <SvgIcon
166
+ name={`file-icon/${fileExtension}`}
167
+ width={24}
168
+ ></SvgIcon>
169
+ )}
170
+
171
+ {isImage(fileExtension) ? (
172
+ <NewDocumentLink
173
+ documentId={item.id}
174
+ documentName={item.name}
175
+ prefix="document"
176
+ >
177
+ {item.name}
178
+ </NewDocumentLink>
179
+ ) : (
180
+ <Button
181
+ type={'text'}
182
+ onClick={handleUserDocumentClick(item.id)}
183
+ >
184
+ {item.name}
185
+ </Button>
186
+ )}
187
+ </Flex>
188
+ </List.Item>
189
+ );
190
+ }}
191
+ />
192
+ )}
193
  </Flex>
194
  </div>
195
  </section>
196
+ {visible && (
197
+ <IndentedTreeModal
198
+ visible={visible}
199
+ hideModal={hideModal}
200
+ documentId={clickedDocumentId}
201
+ ></IndentedTreeModal>
202
+ )}
203
  </div>
204
  );
205
  };
206
 
207
+ export default memo(MessageItem);
web/src/hooks/chunk-hooks.ts CHANGED
@@ -207,12 +207,13 @@ export const useFetchChunk = (chunkId?: string): ResponseType<any> => {
207
  return data;
208
  };
209
 
210
- export const useFetchKnowledgeGraph = (): ResponseType<any> => {
211
- const { documentId } = useGetKnowledgeSearchParams();
212
-
213
  const { data } = useQuery({
214
  queryKey: ['fetchKnowledgeGraph', documentId],
215
  initialData: true,
 
216
  gcTime: 0,
217
  queryFn: async () => {
218
  const data = await kbService.knowledge_graph({
 
207
  return data;
208
  };
209
 
210
+ export const useFetchKnowledgeGraph = (
211
+ documentId: string,
212
+ ): ResponseType<any> => {
213
  const { data } = useQuery({
214
  queryKey: ['fetchKnowledgeGraph', documentId],
215
  initialData: true,
216
+ enabled: !!documentId,
217
  gcTime: 0,
218
  queryFn: async () => {
219
  const data = await kbService.knowledge_graph({
web/src/hooks/common-hooks.tsx CHANGED
@@ -7,16 +7,16 @@ import { useTranslation } from 'react-i18next';
7
  export const useSetModalState = () => {
8
  const [visible, setVisible] = useState(false);
9
 
10
- const showModal = () => {
11
  setVisible(true);
12
- };
13
- const hideModal = () => {
14
  setVisible(false);
15
- };
16
 
17
- const switchVisible = () => {
18
  setVisible(!visible);
19
- };
20
 
21
  return { visible, showModal, hideModal, switchVisible };
22
  };
 
7
  export const useSetModalState = () => {
8
  const [visible, setVisible] = useState(false);
9
 
10
+ const showModal = useCallback(() => {
11
  setVisible(true);
12
+ }, []);
13
+ const hideModal = useCallback(() => {
14
  setVisible(false);
15
+ }, []);
16
 
17
+ const switchVisible = useCallback(() => {
18
  setVisible(!visible);
19
+ }, [visible]);
20
 
21
  return { visible, showModal, hideModal, switchVisible };
22
  };
web/src/hooks/document-hooks.ts CHANGED
@@ -1,7 +1,10 @@
 
1
  import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
2
  import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
 
3
  import { api_host } from '@/utils/api';
4
  import { buildChunkHighlights } from '@/utils/document-util';
 
5
  import { UploadFile } from 'antd';
6
  import { useCallback, useMemo, useState } from 'react';
7
  import { IHighlight } from 'react-pdf-highlighter';
@@ -253,3 +256,37 @@ export const useSelectRunDocumentLoading = () => {
253
  const loading = useOneNamespaceEffectsLoading('kFModel', ['document_run']);
254
  return loading;
255
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IDocumentInfo } from '@/interfaces/database/document';
2
  import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge';
3
  import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
4
+ import kbService from '@/services/knowledge-service';
5
  import { api_host } from '@/utils/api';
6
  import { buildChunkHighlights } from '@/utils/document-util';
7
+ import { useQuery } from '@tanstack/react-query';
8
  import { UploadFile } from 'antd';
9
  import { useCallback, useMemo, useState } from 'react';
10
  import { IHighlight } from 'react-pdf-highlighter';
 
256
  const loading = useOneNamespaceEffectsLoading('kFModel', ['document_run']);
257
  return loading;
258
  };
259
+
260
+ export const useFetchDocumentInfosByIds = () => {
261
+ const [ids, setDocumentIds] = useState<string[]>([]);
262
+ const { data } = useQuery<IDocumentInfo[]>({
263
+ queryKey: ['fetchDocumentInfos', ids],
264
+ enabled: ids.length > 0,
265
+ initialData: [],
266
+ queryFn: async () => {
267
+ const { data } = await kbService.document_infos({ doc_ids: ids });
268
+ if (data.retcode === 0) {
269
+ return data.data;
270
+ }
271
+
272
+ return [];
273
+ },
274
+ });
275
+
276
+ return { data, setDocumentIds };
277
+ };
278
+
279
+ export const useFetchDocumentThumbnailsByIds = () => {
280
+ const [ids, setDocumentIds] = useState<string[]>([]);
281
+ const { data } = useQuery({
282
+ queryKey: ['fetchDocumentThumbnails', ids],
283
+ initialData: [],
284
+ queryFn: async () => {
285
+ const { data } = await kbService.document_thumbnails({ doc_ids: ids });
286
+
287
+ return data;
288
+ },
289
+ });
290
+
291
+ return { data, setDocumentIds };
292
+ };
web/src/interfaces/database/chat.ts CHANGED
@@ -66,6 +66,7 @@ export interface IConversation {
66
  export interface Message {
67
  content: string;
68
  role: MessageType;
 
69
  }
70
 
71
  export interface IReference {
 
66
  export interface Message {
67
  content: string;
68
  role: MessageType;
69
+ doc_ids?: string[];
70
  }
71
 
72
  export interface IReference {
web/src/interfaces/database/document.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface IDocumentInfo {
2
+ chunk_num: number;
3
+ create_date: string;
4
+ create_time: number;
5
+ created_by: string;
6
+ id: string;
7
+ kb_id: string;
8
+ location: string;
9
+ name: string;
10
+ parser_config: Parserconfig;
11
+ parser_id: string;
12
+ process_begin_at: null;
13
+ process_duation: number;
14
+ progress: number;
15
+ progress_msg: string;
16
+ run: string;
17
+ size: number;
18
+ source_type: string;
19
+ status: string;
20
+ thumbnail: string;
21
+ token_num: number;
22
+ type: string;
23
+ update_date: string;
24
+ update_time: number;
25
+ }
26
+
27
+ interface Parserconfig {
28
+ chunk_token_num: number;
29
+ layout_recognize: boolean;
30
+ raptor: Raptor;
31
+ }
32
+
33
+ interface Raptor {
34
+ use_raptor: boolean;
35
+ }
web/src/pages/add-knowledge/components/knowledge-chunk/components/knowledge-graph/modal.tsx CHANGED
@@ -1,9 +1,10 @@
 
1
  import { useFetchKnowledgeGraph } from '@/hooks/chunk-hooks';
 
2
  import { Flex, Modal, Segmented } from 'antd';
3
  import React, { useEffect, useMemo, useState } from 'react';
4
  import { useTranslation } from 'react-i18next';
5
  import ForceGraph from './force-graph';
6
- import IndentedTree from './indented-tree';
7
  import styles from './index.less';
8
  import { isDataExist } from './util';
9
 
@@ -14,7 +15,8 @@ enum SegmentedValue {
14
 
15
  const KnowledgeGraphModal: React.FC = () => {
16
  const [isModalOpen, setIsModalOpen] = useState(false);
17
- const { data } = useFetchKnowledgeGraph();
 
18
  const [value, setValue] = useState<SegmentedValue>(SegmentedValue.Graph);
19
  const { t } = useTranslation();
20
 
 
1
+ import IndentedTree from '@/components/indented-tree/indented-tree';
2
  import { useFetchKnowledgeGraph } from '@/hooks/chunk-hooks';
3
+ import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
4
  import { Flex, Modal, Segmented } from 'antd';
5
  import React, { useEffect, useMemo, useState } from 'react';
6
  import { useTranslation } from 'react-i18next';
7
  import ForceGraph from './force-graph';
 
8
  import styles from './index.less';
9
  import { isDataExist } from './util';
10
 
 
15
 
16
  const KnowledgeGraphModal: React.FC = () => {
17
  const [isModalOpen, setIsModalOpen] = useState(false);
18
+ const { documentId } = useGetKnowledgeSearchParams();
19
+ const { data } = useFetchKnowledgeGraph(documentId);
20
  const [value, setValue] = useState<SegmentedValue>(SegmentedValue.Graph);
21
  const { t } = useTranslation();
22
 
web/src/pages/chat/chat-container/index.tsx CHANGED
@@ -1,8 +1,7 @@
1
  import MessageItem from '@/components/message-item';
2
  import DocumentPreviewer from '@/components/pdf-previewer';
3
  import { MessageType } from '@/constants/chat';
4
- import { useTranslate } from '@/hooks/common-hooks';
5
- import { Button, Drawer, Flex, Input, Spin } from 'antd';
6
  import {
7
  useClickDrawer,
8
  useFetchConversationOnMount,
@@ -14,6 +13,7 @@ import {
14
  } from '../hooks';
15
  import { buildMessageItemReference } from '../utils';
16
 
 
17
  import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
18
  import styles from './index.less';
19
 
@@ -42,7 +42,6 @@ const ChatContainer = () => {
42
  const sendDisabled = useSendButtonDisabled(value);
43
  useGetFileIcon();
44
  const loading = useSelectConversationLoading();
45
- const { t } = useTranslate('chat');
46
  const { data: userInfo } = useFetchUserInfo();
47
 
48
  return (
@@ -72,7 +71,16 @@ const ChatContainer = () => {
72
  </div>
73
  <div ref={ref} />
74
  </Flex>
75
- <Input
 
 
 
 
 
 
 
 
 
76
  size="large"
77
  placeholder={t('sendPlaceholder')}
78
  value={value}
@@ -89,7 +97,7 @@ const ChatContainer = () => {
89
  }
90
  onPressEnter={handlePressEnter}
91
  onChange={handleInputChange}
92
- />
93
  </Flex>
94
  <Drawer
95
  title="Document Previewer"
 
1
  import MessageItem from '@/components/message-item';
2
  import DocumentPreviewer from '@/components/pdf-previewer';
3
  import { MessageType } from '@/constants/chat';
4
+ import { Drawer, Flex, Spin } from 'antd';
 
5
  import {
6
  useClickDrawer,
7
  useFetchConversationOnMount,
 
13
  } from '../hooks';
14
  import { buildMessageItemReference } from '../utils';
15
 
16
+ import MessageInput from '@/components/message-input';
17
  import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
18
  import styles from './index.less';
19
 
 
42
  const sendDisabled = useSendButtonDisabled(value);
43
  useGetFileIcon();
44
  const loading = useSelectConversationLoading();
 
45
  const { data: userInfo } = useFetchUserInfo();
46
 
47
  return (
 
71
  </div>
72
  <div ref={ref} />
73
  </Flex>
74
+ <MessageInput
75
+ disabled={disabled}
76
+ sendDisabled={sendDisabled}
77
+ sendLoading={sendLoading}
78
+ value={value}
79
+ onInputChange={handleInputChange}
80
+ onPressEnter={handlePressEnter}
81
+ conversationId={conversation.id}
82
+ ></MessageInput>
83
+ {/* <Input
84
  size="large"
85
  placeholder={t('sendPlaceholder')}
86
  value={value}
 
97
  }
98
  onPressEnter={handlePressEnter}
99
  onChange={handleInputChange}
100
+ /> */}
101
  </Flex>
102
  <Drawer
103
  title="Document Previewer"
web/src/pages/chat/hooks.ts CHANGED
@@ -547,7 +547,7 @@ export const useSendMessage = (
547
  const { send, answer, done, setDone } = useSendMessageWithSse();
548
 
549
  const sendMessage = useCallback(
550
- async (message: string, id?: string) => {
551
  const res = await send({
552
  conversation_id: id ?? conversationId,
553
  messages: [
@@ -555,6 +555,7 @@ export const useSendMessage = (
555
  {
556
  role: MessageType.User,
557
  content: message,
 
558
  },
559
  ],
560
  });
@@ -586,14 +587,14 @@ export const useSendMessage = (
586
  );
587
 
588
  const handleSendMessage = useCallback(
589
- async (message: string) => {
590
  if (conversationId !== '') {
591
- sendMessage(message);
592
  } else {
593
  const data = await setConversation(message);
594
  if (data.retcode === 0) {
595
  const id = data.data.id;
596
- sendMessage(message, id);
597
  }
598
  }
599
  },
@@ -614,15 +615,19 @@ export const useSendMessage = (
614
  }
615
  }, [setDone, conversationId]);
616
 
617
- const handlePressEnter = useCallback(() => {
618
- if (trim(value) === '') return;
619
-
620
- if (done) {
621
- setValue('');
622
- handleSendMessage(value.trim());
623
- }
624
- addNewestConversation(value);
625
- }, [addNewestConversation, handleSendMessage, done, setValue, value]);
 
 
 
 
626
 
627
  return {
628
  handlePressEnter,
 
547
  const { send, answer, done, setDone } = useSendMessageWithSse();
548
 
549
  const sendMessage = useCallback(
550
+ async (message: string, documentIds: string[], id?: string) => {
551
  const res = await send({
552
  conversation_id: id ?? conversationId,
553
  messages: [
 
555
  {
556
  role: MessageType.User,
557
  content: message,
558
+ doc_ids: documentIds,
559
  },
560
  ],
561
  });
 
587
  );
588
 
589
  const handleSendMessage = useCallback(
590
+ async (message: string, documentIds: string[]) => {
591
  if (conversationId !== '') {
592
+ return sendMessage(message, documentIds);
593
  } else {
594
  const data = await setConversation(message);
595
  if (data.retcode === 0) {
596
  const id = data.data.id;
597
+ return sendMessage(message, documentIds, id);
598
  }
599
  }
600
  },
 
615
  }
616
  }, [setDone, conversationId]);
617
 
618
+ const handlePressEnter = useCallback(
619
+ async (documentIds: string[]) => {
620
+ if (trim(value) === '') return;
621
+ let ret;
622
+ if (done) {
623
+ setValue('');
624
+ ret = await handleSendMessage(value.trim(), documentIds);
625
+ }
626
+ addNewestConversation(value);
627
+ return ret;
628
+ },
629
+ [addNewestConversation, handleSendMessage, done, setValue, value],
630
+ );
631
 
632
  return {
633
  handlePressEnter,
web/src/pages/force-graph/index.tsx CHANGED
@@ -2,6 +2,7 @@ import { Graph } from '@antv/g6';
2
  import { useSize } from 'ahooks';
3
  import { useEffect, useRef } from 'react';
4
  import { graphData } from './constant';
 
5
 
6
  import styles from './index.less';
7
  import { Converter } from './util';
@@ -108,4 +109,4 @@ const ForceGraph = () => {
108
  return <div ref={containerRef} className={styles.container} />;
109
  };
110
 
111
- export default ForceGraph;
 
2
  import { useSize } from 'ahooks';
3
  import { useEffect, useRef } from 'react';
4
  import { graphData } from './constant';
5
+ import InputWithUpload from './input-upload';
6
 
7
  import styles from './index.less';
8
  import { Converter } from './util';
 
109
  return <div ref={containerRef} className={styles.container} />;
110
  };
111
 
112
+ export default InputWithUpload;
web/src/pages/force-graph/input-upload.tsx ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Authorization } from '@/constants/authorization';
2
+ import { getAuthorization } from '@/utils/authorization-util';
3
+ import { PlusOutlined } from '@ant-design/icons';
4
+ import type { GetProp, UploadFile, UploadProps } from 'antd';
5
+ import { Image, Input, Upload } from 'antd';
6
+ import { useState } from 'react';
7
+ import { useGetChatSearchParams } from '../chat/hooks';
8
+
9
+ type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
10
+
11
+ const getBase64 = (file: FileType): Promise<string> =>
12
+ new Promise((resolve, reject) => {
13
+ const reader = new FileReader();
14
+ reader.readAsDataURL(file);
15
+ reader.onload = () => resolve(reader.result as string);
16
+ reader.onerror = (error) => reject(error);
17
+ });
18
+
19
+ const InputWithUpload = () => {
20
+ const [previewOpen, setPreviewOpen] = useState(false);
21
+ const [previewImage, setPreviewImage] = useState('');
22
+ const { conversationId } = useGetChatSearchParams();
23
+ const [fileList, setFileList] = useState<UploadFile[]>([
24
+ {
25
+ uid: '-1',
26
+ name: 'image.png',
27
+ status: 'done',
28
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
29
+ },
30
+ {
31
+ uid: '-2',
32
+ name: 'image.png',
33
+ status: 'done',
34
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
35
+ },
36
+ {
37
+ uid: '-3',
38
+ name: 'image.png',
39
+ status: 'done',
40
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
41
+ },
42
+ {
43
+ uid: '-4',
44
+ name: 'image.png',
45
+ status: 'done',
46
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
47
+ },
48
+ {
49
+ uid: '-xxx',
50
+ percent: 50,
51
+ name: 'image.png',
52
+ status: 'uploading',
53
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
54
+ },
55
+ {
56
+ uid: '-5',
57
+ name: 'image.png',
58
+ status: 'error',
59
+ },
60
+ ]);
61
+
62
+ const handlePreview = async (file: UploadFile) => {
63
+ if (!file.url && !file.preview) {
64
+ file.preview = await getBase64(file.originFileObj as FileType);
65
+ }
66
+
67
+ setPreviewImage(file.url || (file.preview as string));
68
+ setPreviewOpen(true);
69
+ };
70
+
71
+ const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
72
+ setFileList(newFileList);
73
+
74
+ const uploadButton = (
75
+ <button style={{ border: 0, background: 'none' }} type="button">
76
+ <PlusOutlined />
77
+ <div style={{ marginTop: 8 }}>Upload</div>
78
+ </button>
79
+ );
80
+ return (
81
+ <>
82
+ <Input placeholder="Basic usage"></Input>
83
+ <Upload
84
+ action="/v1/document/upload_and_parse"
85
+ listType="picture-card"
86
+ fileList={fileList}
87
+ onPreview={handlePreview}
88
+ onChange={handleChange}
89
+ multiple
90
+ headers={{ [Authorization]: getAuthorization() }}
91
+ data={{ conversation_id: '9e9f7d2453e511efb18efa163e197198' }}
92
+ method="post"
93
+ >
94
+ {fileList.length >= 8 ? null : uploadButton}
95
+ </Upload>
96
+ {previewImage && (
97
+ <Image
98
+ wrapperStyle={{ display: 'none' }}
99
+ preview={{
100
+ visible: previewOpen,
101
+ onVisibleChange: (visible) => setPreviewOpen(visible),
102
+ afterOpenChange: (visible) => !visible && setPreviewImage(''),
103
+ }}
104
+ src={previewImage}
105
+ />
106
+ )}
107
+ </>
108
+ );
109
+ };
110
+
111
+ export default () => {
112
+ return (
113
+ <section style={{ height: 500, width: 400 }}>
114
+ <div style={{ height: 200 }}></div>
115
+ <InputWithUpload></InputWithUpload>
116
+ </section>
117
+ );
118
+ };
web/src/services/knowledge-service.ts CHANGED
@@ -28,6 +28,7 @@ const {
28
  document_upload,
29
  web_crawl,
30
  knowledge_graph,
 
31
  } = api;
32
 
33
  const methods = {
@@ -93,6 +94,10 @@ const methods = {
93
  url: web_crawl,
94
  method: 'post',
95
  },
 
 
 
 
96
  // chunk管理
97
  chunk_list: {
98
  url: chunk_list,
 
28
  document_upload,
29
  web_crawl,
30
  knowledge_graph,
31
+ document_infos,
32
  } = api;
33
 
34
  const methods = {
 
94
  url: web_crawl,
95
  method: 'post',
96
  },
97
+ document_infos: {
98
+ url: document_infos,
99
+ method: 'post',
100
+ },
101
  // chunk管理
102
  chunk_list: {
103
  url: chunk_list,
web/src/utils/api.ts CHANGED
@@ -38,7 +38,6 @@ export default {
38
  knowledge_graph: `${api_host}/chunk/knowledge_graph`,
39
 
40
  // document
41
- upload: `${api_host}/document/upload`,
42
  get_document_list: `${api_host}/document/list`,
43
  document_change_status: `${api_host}/document/change_status`,
44
  document_rm: `${api_host}/document/rm`,
@@ -50,6 +49,7 @@ export default {
50
  get_document_file: `${api_host}/document/get`,
51
  document_upload: `${api_host}/document/upload`,
52
  web_crawl: `${api_host}/document/web_crawl`,
 
53
 
54
  // chat
55
  setDialog: `${api_host}/dialog/set`,
 
38
  knowledge_graph: `${api_host}/chunk/knowledge_graph`,
39
 
40
  // document
 
41
  get_document_list: `${api_host}/document/list`,
42
  document_change_status: `${api_host}/document/change_status`,
43
  document_rm: `${api_host}/document/rm`,
 
49
  get_document_file: `${api_host}/document/get`,
50
  document_upload: `${api_host}/document/upload`,
51
  web_crawl: `${api_host}/document/web_crawl`,
52
+ document_infos: `${api_host}/document/infos`,
53
 
54
  // chat
55
  setDialog: `${api_host}/dialog/set`,
web/src/utils/document-util.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SupportedPreviewDocumentTypes } from '@/constants/common';
2
  import { IChunk } from '@/interfaces/database/knowledge';
3
  import { UploadFile } from 'antd';
4
  import { v4 as uuid } from 'uuid';
@@ -51,3 +51,7 @@ export const getUnSupportedFilesCount = (message: string) => {
51
  export const isSupportedPreviewDocumentType = (fileExtension: string) => {
52
  return SupportedPreviewDocumentTypes.includes(fileExtension);
53
  };
 
 
 
 
 
1
+ import { Images, SupportedPreviewDocumentTypes } from '@/constants/common';
2
  import { IChunk } from '@/interfaces/database/knowledge';
3
  import { UploadFile } from 'antd';
4
  import { v4 as uuid } from 'uuid';
 
51
  export const isSupportedPreviewDocumentType = (fileExtension: string) => {
52
  return SupportedPreviewDocumentTypes.includes(fileExtension);
53
  };
54
+
55
+ export const isImage = (image: string) => {
56
+ return [...Images, 'svg'].some((x) => x === image);
57
+ };