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 +0 -0
- web/src/components/indented-tree/modal.tsx +31 -0
- web/src/components/message-input/index.tsx +129 -0
- web/src/components/message-item/index.tsx +78 -5
- web/src/hooks/chunk-hooks.ts +4 -3
- web/src/hooks/common-hooks.tsx +6 -6
- web/src/hooks/document-hooks.ts +37 -0
- web/src/interfaces/database/chat.ts +1 -0
- web/src/interfaces/database/document.ts +35 -0
- web/src/pages/add-knowledge/components/knowledge-chunk/components/knowledge-graph/modal.tsx +4 -2
- web/src/pages/chat/chat-container/index.tsx +13 -5
- web/src/pages/chat/hooks.ts +18 -13
- web/src/pages/force-graph/index.tsx +2 -1
- web/src/pages/force-graph/input-upload.tsx +118 -0
- web/src/services/knowledge-service.ts +5 -0
- web/src/utils/api.ts +1 -1
- web/src/utils/document-util.ts +5 -1
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 = (
|
211 |
-
|
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 {
|
|
|
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 {
|
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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
619 |
-
|
620 |
-
|
621 |
-
|
622 |
-
|
623 |
-
|
624 |
-
|
625 |
-
|
|
|
|
|
|
|
|
|
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
|
|
|
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 |
+
};
|