balibabu
commited on
Commit
·
700dff9
1
Parent(s):
af9205c
fix: replace some pictures of chunk method #437 (#438)
Browse files### What problem does this PR solve?
some chunk method pictures are not in English #437
feat: set the height of both html and body to 100%
feat: add SharedChat
feat: add shared hooks
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- web/src/assets/svg/chunk-method/law-02.svg +0 -0
- web/src/assets/svg/chunk-method/manual-02.svg +0 -0
- web/src/assets/svg/chunk-method/manual-04.svg +0 -0
- web/src/assets/svg/chunk-method/qa-01.svg +0 -0
- web/src/assets/svg/chunk-method/qa-02.svg +0 -0
- web/src/assets/svg/chunk-method/resume-02.svg +0 -0
- web/src/assets/svg/chunk-method/table-01.svg +0 -0
- web/src/assets/svg/chunk-method/table-02.svg +0 -0
- web/src/global.less +13 -0
- web/src/hooks/chatHooks.ts +76 -1
- web/src/pages/chat/chat-container/index.less +22 -22
- web/src/pages/chat/chat-container/index.tsx +7 -157
- web/src/pages/chat/chat-overview-modal/index.tsx +21 -7
- web/src/pages/chat/hooks.ts +6 -1
- web/src/pages/chat/markdown-content/index.less +25 -0
- web/src/pages/chat/markdown-content/index.tsx +173 -0
- web/src/pages/chat/model.ts +10 -10
- web/src/pages/chat/share/index.less +50 -0
- web/src/pages/chat/share/index.tsx +53 -0
- web/src/pages/chat/share/large.tsx +122 -0
- web/src/pages/chat/share/shared-markdown.tsx +32 -0
- web/src/pages/chat/shared-hooks.ts +192 -0
- web/src/routes.ts +5 -0
- web/src/services/chatService.ts +1 -1
- web/src/utils/commonUtil.ts +5 -0
- web/src/utils/request.ts +5 -2
web/src/assets/svg/chunk-method/law-02.svg
CHANGED
|
|
web/src/assets/svg/chunk-method/manual-02.svg
CHANGED
|
|
web/src/assets/svg/chunk-method/manual-04.svg
CHANGED
|
|
web/src/assets/svg/chunk-method/qa-01.svg
CHANGED
|
|
web/src/assets/svg/chunk-method/qa-02.svg
CHANGED
|
|
web/src/assets/svg/chunk-method/resume-02.svg
CHANGED
|
|
web/src/assets/svg/chunk-method/table-01.svg
CHANGED
|
|
web/src/assets/svg/chunk-method/table-02.svg
CHANGED
|
|
web/src/global.less
CHANGED
@@ -1,6 +1,19 @@
|
|
1 |
@import url(./inter.less);
|
2 |
|
|
|
|
|
|
|
|
|
3 |
body {
|
4 |
font-family: Inter;
|
5 |
margin: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
}
|
|
|
1 |
@import url(./inter.less);
|
2 |
|
3 |
+
html {
|
4 |
+
height: 100%;
|
5 |
+
}
|
6 |
+
|
7 |
body {
|
8 |
font-family: Inter;
|
9 |
margin: 0;
|
10 |
+
height: 100%;
|
11 |
+
}
|
12 |
+
|
13 |
+
#root {
|
14 |
+
height: 100%;
|
15 |
+
}
|
16 |
+
|
17 |
+
.ant-app {
|
18 |
+
height: 100%;
|
19 |
}
|
web/src/hooks/chatHooks.ts
CHANGED
@@ -4,7 +4,7 @@ import {
|
|
4 |
IStats,
|
5 |
IToken,
|
6 |
} from '@/interfaces/database/chat';
|
7 |
-
import { useCallback } from 'react';
|
8 |
import { useDispatch, useSelector } from 'umi';
|
9 |
|
10 |
export const useFetchDialogList = () => {
|
@@ -248,3 +248,78 @@ export const useSelectStats = () => {
|
|
248 |
};
|
249 |
|
250 |
//#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
IStats,
|
5 |
IToken,
|
6 |
} from '@/interfaces/database/chat';
|
7 |
+
import { useCallback, useEffect, useState } from 'react';
|
8 |
import { useDispatch, useSelector } from 'umi';
|
9 |
|
10 |
export const useFetchDialogList = () => {
|
|
|
248 |
};
|
249 |
|
250 |
//#endregion
|
251 |
+
|
252 |
+
//#region shared chat
|
253 |
+
|
254 |
+
export const useCreateSharedConversation = () => {
|
255 |
+
const dispatch = useDispatch();
|
256 |
+
|
257 |
+
const createSharedConversation = useCallback(
|
258 |
+
(userId?: string) => {
|
259 |
+
return dispatch<any>({
|
260 |
+
type: 'chatModel/createExternalConversation',
|
261 |
+
payload: { userId },
|
262 |
+
});
|
263 |
+
},
|
264 |
+
[dispatch],
|
265 |
+
);
|
266 |
+
|
267 |
+
return createSharedConversation;
|
268 |
+
};
|
269 |
+
|
270 |
+
export const useFetchSharedConversation = () => {
|
271 |
+
const dispatch = useDispatch();
|
272 |
+
|
273 |
+
const fetchSharedConversation = useCallback(
|
274 |
+
(conversationId: string) => {
|
275 |
+
return dispatch<any>({
|
276 |
+
type: 'chatModel/getExternalConversation',
|
277 |
+
payload: conversationId,
|
278 |
+
});
|
279 |
+
},
|
280 |
+
[dispatch],
|
281 |
+
);
|
282 |
+
|
283 |
+
return fetchSharedConversation;
|
284 |
+
};
|
285 |
+
|
286 |
+
export const useCompleteSharedConversation = () => {
|
287 |
+
const dispatch = useDispatch();
|
288 |
+
|
289 |
+
const completeSharedConversation = useCallback(
|
290 |
+
(payload: any) => {
|
291 |
+
return dispatch<any>({
|
292 |
+
type: 'chatModel/completeExternalConversation',
|
293 |
+
payload: payload,
|
294 |
+
});
|
295 |
+
},
|
296 |
+
[dispatch],
|
297 |
+
);
|
298 |
+
|
299 |
+
return completeSharedConversation;
|
300 |
+
};
|
301 |
+
|
302 |
+
export const useCreatePublicUrlToken = (dialogId: string, visible: boolean) => {
|
303 |
+
const [token, setToken] = useState();
|
304 |
+
const createToken = useCreateToken(dialogId);
|
305 |
+
const { protocol, host } = window.location;
|
306 |
+
|
307 |
+
const urlWithToken = `${protocol}//${host}/chat/share?shared_id=${token}`;
|
308 |
+
|
309 |
+
const createUrlToken = useCallback(async () => {
|
310 |
+
if (visible) {
|
311 |
+
const data = await createToken();
|
312 |
+
const urlToken = data.data?.token;
|
313 |
+
if (urlToken) {
|
314 |
+
setToken(urlToken);
|
315 |
+
}
|
316 |
+
}
|
317 |
+
}, [createToken, visible]);
|
318 |
+
|
319 |
+
useEffect(() => {
|
320 |
+
createUrlToken();
|
321 |
+
}, [createUrlToken]);
|
322 |
+
|
323 |
+
return { token, createUrlToken, urlWithToken };
|
324 |
+
};
|
325 |
+
//#endregion
|
web/src/pages/chat/chat-container/index.less
CHANGED
@@ -33,9 +33,9 @@
|
|
33 |
.messageEmpty {
|
34 |
width: 300px;
|
35 |
}
|
36 |
-
.referenceIcon {
|
37 |
-
|
38 |
-
}
|
39 |
}
|
40 |
|
41 |
.messageItemLeft {
|
@@ -46,24 +46,24 @@
|
|
46 |
text-align: right;
|
47 |
}
|
48 |
|
49 |
-
.referencePopoverWrapper {
|
50 |
-
|
51 |
-
}
|
52 |
|
53 |
-
.referenceChunkImage {
|
54 |
-
|
55 |
-
|
56 |
-
}
|
57 |
|
58 |
-
.referenceImagePreview {
|
59 |
-
|
60 |
-
|
61 |
-
}
|
62 |
-
.chunkContentText {
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
}
|
67 |
-
.documentLink {
|
68 |
-
|
69 |
-
}
|
|
|
33 |
.messageEmpty {
|
34 |
width: 300px;
|
35 |
}
|
36 |
+
// .referenceIcon {
|
37 |
+
// padding: 0 6px;
|
38 |
+
// }
|
39 |
}
|
40 |
|
41 |
.messageItemLeft {
|
|
|
46 |
text-align: right;
|
47 |
}
|
48 |
|
49 |
+
// .referencePopoverWrapper {
|
50 |
+
// max-width: 50vw;
|
51 |
+
// }
|
52 |
|
53 |
+
// .referenceChunkImage {
|
54 |
+
// width: 10vw;
|
55 |
+
// object-fit: contain;
|
56 |
+
// }
|
57 |
|
58 |
+
// .referenceImagePreview {
|
59 |
+
// max-width: 45vw;
|
60 |
+
// max-height: 45vh;
|
61 |
+
// }
|
62 |
+
// .chunkContentText {
|
63 |
+
// .chunkText;
|
64 |
+
// max-height: 45vh;
|
65 |
+
// overflow-y: auto;
|
66 |
+
// }
|
67 |
+
// .documentLink {
|
68 |
+
// padding: 0;
|
69 |
+
// }
|
web/src/pages/chat/chat-container/index.tsx
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
2 |
-
import Image from '@/components/image';
|
3 |
import NewDocumentLink from '@/components/new-document-link';
|
4 |
import DocumentPreviewer from '@/components/pdf-previewer';
|
5 |
import { MessageType } from '@/constants/chat';
|
@@ -7,7 +6,6 @@ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
|
|
7 |
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
8 |
import { IReference, Message } from '@/interfaces/database/chat';
|
9 |
import { IChunk } from '@/interfaces/database/knowledge';
|
10 |
-
import { InfoCircleOutlined } from '@ant-design/icons';
|
11 |
import {
|
12 |
Avatar,
|
13 |
Button,
|
@@ -15,18 +13,11 @@ import {
|
|
15 |
Flex,
|
16 |
Input,
|
17 |
List,
|
18 |
-
Popover,
|
19 |
Skeleton,
|
20 |
-
Space,
|
21 |
Spin,
|
22 |
} from 'antd';
|
23 |
import classNames from 'classnames';
|
24 |
-
import {
|
25 |
-
import Markdown from 'react-markdown';
|
26 |
-
import reactStringReplace from 'react-string-replace';
|
27 |
-
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
28 |
-
import remarkGfm from 'remark-gfm';
|
29 |
-
import { visitParents } from 'unist-util-visit-parents';
|
30 |
import {
|
31 |
useClickDrawer,
|
32 |
useFetchConversationOnMount,
|
@@ -35,33 +26,13 @@ import {
|
|
35 |
useSelectConversationLoading,
|
36 |
useSendMessage,
|
37 |
} from '../hooks';
|
|
|
38 |
|
39 |
import SvgIcon from '@/components/svg-icon';
|
40 |
import { useTranslate } from '@/hooks/commonHooks';
|
41 |
import { getExtension, isPdf } from '@/utils/documentUtils';
|
42 |
import styles from './index.less';
|
43 |
|
44 |
-
const reg = /(#{2}\d+\${2})/g;
|
45 |
-
|
46 |
-
const getChunkIndex = (match: string) => Number(match.slice(2, -2));
|
47 |
-
|
48 |
-
const rehypeWrapReference = () => {
|
49 |
-
return function wrapTextTransform(tree: any) {
|
50 |
-
visitParents(tree, 'text', (node, ancestors) => {
|
51 |
-
const latestAncestor = ancestors.at(-1);
|
52 |
-
if (
|
53 |
-
latestAncestor.tagName !== 'custom-typography' &&
|
54 |
-
latestAncestor.tagName !== 'code'
|
55 |
-
) {
|
56 |
-
node.type = 'element';
|
57 |
-
node.tagName = 'custom-typography';
|
58 |
-
node.properties = {};
|
59 |
-
node.children = [{ type: 'text', value: node.value }];
|
60 |
-
}
|
61 |
-
});
|
62 |
-
};
|
63 |
-
};
|
64 |
-
|
65 |
const MessageItem = ({
|
66 |
item,
|
67 |
reference,
|
@@ -76,100 +47,6 @@ const MessageItem = ({
|
|
76 |
|
77 |
const isAssistant = item.role === MessageType.Assistant;
|
78 |
|
79 |
-
const handleDocumentButtonClick = useCallback(
|
80 |
-
(documentId: string, chunk: IChunk, isPdf: boolean) => () => {
|
81 |
-
if (!isPdf) {
|
82 |
-
return;
|
83 |
-
}
|
84 |
-
clickDocumentButton(documentId, chunk);
|
85 |
-
},
|
86 |
-
[clickDocumentButton],
|
87 |
-
);
|
88 |
-
|
89 |
-
const getPopoverContent = useCallback(
|
90 |
-
(chunkIndex: number) => {
|
91 |
-
const chunks = reference?.chunks ?? [];
|
92 |
-
const chunkItem = chunks[chunkIndex];
|
93 |
-
const document = reference?.doc_aggs.find(
|
94 |
-
(x) => x?.doc_id === chunkItem?.doc_id,
|
95 |
-
);
|
96 |
-
const documentId = document?.doc_id;
|
97 |
-
const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
|
98 |
-
const fileExtension = documentId ? getExtension(document?.doc_name) : '';
|
99 |
-
const imageId = chunkItem?.img_id;
|
100 |
-
return (
|
101 |
-
<Flex
|
102 |
-
key={chunkItem?.chunk_id}
|
103 |
-
gap={10}
|
104 |
-
className={styles.referencePopoverWrapper}
|
105 |
-
>
|
106 |
-
{imageId && (
|
107 |
-
<Popover
|
108 |
-
placement="left"
|
109 |
-
content={
|
110 |
-
<Image
|
111 |
-
id={imageId}
|
112 |
-
className={styles.referenceImagePreview}
|
113 |
-
></Image>
|
114 |
-
}
|
115 |
-
>
|
116 |
-
<Image
|
117 |
-
id={imageId}
|
118 |
-
className={styles.referenceChunkImage}
|
119 |
-
></Image>
|
120 |
-
</Popover>
|
121 |
-
)}
|
122 |
-
<Space direction={'vertical'}>
|
123 |
-
<div
|
124 |
-
dangerouslySetInnerHTML={{
|
125 |
-
__html: chunkItem?.content_with_weight,
|
126 |
-
}}
|
127 |
-
className={styles.chunkContentText}
|
128 |
-
></div>
|
129 |
-
{documentId && (
|
130 |
-
<Flex gap={'small'}>
|
131 |
-
{fileThumbnail ? (
|
132 |
-
<img src={fileThumbnail} alt="" />
|
133 |
-
) : (
|
134 |
-
<SvgIcon
|
135 |
-
name={`file-icon/${fileExtension}`}
|
136 |
-
width={24}
|
137 |
-
></SvgIcon>
|
138 |
-
)}
|
139 |
-
<Button
|
140 |
-
type="link"
|
141 |
-
className={styles.documentLink}
|
142 |
-
onClick={handleDocumentButtonClick(
|
143 |
-
documentId,
|
144 |
-
chunkItem,
|
145 |
-
fileExtension === 'pdf',
|
146 |
-
)}
|
147 |
-
>
|
148 |
-
{document?.doc_name}
|
149 |
-
</Button>
|
150 |
-
</Flex>
|
151 |
-
)}
|
152 |
-
</Space>
|
153 |
-
</Flex>
|
154 |
-
);
|
155 |
-
},
|
156 |
-
[reference, fileThumbnails, handleDocumentButtonClick],
|
157 |
-
);
|
158 |
-
|
159 |
-
const renderReference = useCallback(
|
160 |
-
(text: string) => {
|
161 |
-
return reactStringReplace(text, reg, (match, i) => {
|
162 |
-
const chunkIndex = getChunkIndex(match);
|
163 |
-
return (
|
164 |
-
<Popover content={getPopoverContent(chunkIndex)}>
|
165 |
-
<InfoCircleOutlined key={i} className={styles.referenceIcon} />
|
166 |
-
</Popover>
|
167 |
-
);
|
168 |
-
});
|
169 |
-
},
|
170 |
-
[getPopoverContent],
|
171 |
-
);
|
172 |
-
|
173 |
const referenceDocumentList = useMemo(() => {
|
174 |
return reference?.doc_aggs ?? [];
|
175 |
}, [reference?.doc_aggs]);
|
@@ -207,38 +84,11 @@ const MessageItem = ({
|
|
207 |
<b>{isAssistant ? '' : userInfo.nickname}</b>
|
208 |
<div className={styles.messageText}>
|
209 |
{item.content !== '' ? (
|
210 |
-
<
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
'custom-typography': ({
|
216 |
-
children,
|
217 |
-
}: {
|
218 |
-
children: string;
|
219 |
-
}) => renderReference(children),
|
220 |
-
code(props: any) {
|
221 |
-
const { children, className, node, ...rest } = props;
|
222 |
-
const match = /language-(\w+)/.exec(className || '');
|
223 |
-
return match ? (
|
224 |
-
<SyntaxHighlighter
|
225 |
-
{...rest}
|
226 |
-
PreTag="div"
|
227 |
-
language={match[1]}
|
228 |
-
>
|
229 |
-
{String(children).replace(/\n$/, '')}
|
230 |
-
</SyntaxHighlighter>
|
231 |
-
) : (
|
232 |
-
<code {...rest} className={className}>
|
233 |
-
{children}
|
234 |
-
</code>
|
235 |
-
);
|
236 |
-
},
|
237 |
-
} as any
|
238 |
-
}
|
239 |
-
>
|
240 |
-
{item.content}
|
241 |
-
</Markdown>
|
242 |
) : (
|
243 |
<Skeleton active className={styles.messageEmpty} />
|
244 |
)}
|
|
|
1 |
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
|
|
2 |
import NewDocumentLink from '@/components/new-document-link';
|
3 |
import DocumentPreviewer from '@/components/pdf-previewer';
|
4 |
import { MessageType } from '@/constants/chat';
|
|
|
6 |
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
7 |
import { IReference, Message } from '@/interfaces/database/chat';
|
8 |
import { IChunk } from '@/interfaces/database/knowledge';
|
|
|
9 |
import {
|
10 |
Avatar,
|
11 |
Button,
|
|
|
13 |
Flex,
|
14 |
Input,
|
15 |
List,
|
|
|
16 |
Skeleton,
|
|
|
17 |
Spin,
|
18 |
} from 'antd';
|
19 |
import classNames from 'classnames';
|
20 |
+
import { useMemo } from 'react';
|
|
|
|
|
|
|
|
|
|
|
21 |
import {
|
22 |
useClickDrawer,
|
23 |
useFetchConversationOnMount,
|
|
|
26 |
useSelectConversationLoading,
|
27 |
useSendMessage,
|
28 |
} from '../hooks';
|
29 |
+
import MarkdownContent from '../markdown-content';
|
30 |
|
31 |
import SvgIcon from '@/components/svg-icon';
|
32 |
import { useTranslate } from '@/hooks/commonHooks';
|
33 |
import { getExtension, isPdf } from '@/utils/documentUtils';
|
34 |
import styles from './index.less';
|
35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
const MessageItem = ({
|
37 |
item,
|
38 |
reference,
|
|
|
47 |
|
48 |
const isAssistant = item.role === MessageType.Assistant;
|
49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
const referenceDocumentList = useMemo(() => {
|
51 |
return reference?.doc_aggs ?? [];
|
52 |
}, [reference?.doc_aggs]);
|
|
|
84 |
<b>{isAssistant ? '' : userInfo.nickname}</b>
|
85 |
<div className={styles.messageText}>
|
86 |
{item.content !== '' ? (
|
87 |
+
<MarkdownContent
|
88 |
+
content={item.content}
|
89 |
+
reference={reference}
|
90 |
+
clickDocumentButton={clickDocumentButton}
|
91 |
+
></MarkdownContent>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
) : (
|
93 |
<Skeleton active className={styles.messageEmpty} />
|
94 |
)}
|
web/src/pages/chat/chat-overview-modal/index.tsx
CHANGED
@@ -1,11 +1,15 @@
|
|
|
|
1 |
import LineChart from '@/components/line-chart';
|
|
|
2 |
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
|
3 |
import { IModalProps } from '@/interfaces/common';
|
4 |
import { IDialog, IStats } from '@/interfaces/database/chat';
|
|
|
5 |
import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd';
|
6 |
import { RangePickerProps } from 'antd/es/date-picker';
|
7 |
import dayjs from 'dayjs';
|
8 |
import camelCase from 'lodash/camelCase';
|
|
|
9 |
import ChatApiKeyModal from '../chat-api-key-modal';
|
10 |
import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks';
|
11 |
import styles from './index.less';
|
@@ -20,6 +24,10 @@ const ChatOverviewModal = ({
|
|
20 |
}: IModalProps<any> & { dialog: IDialog }) => {
|
21 |
const { t } = useTranslate('chat');
|
22 |
const chartList = useSelectChartStatsList();
|
|
|
|
|
|
|
|
|
23 |
|
24 |
const {
|
25 |
visible: apiKeyVisible,
|
@@ -45,14 +53,20 @@ const ChatOverviewModal = ({
|
|
45 |
<Card title={dialog.name}>
|
46 |
<Flex gap={8} vertical>
|
47 |
{t('publicUrl')}
|
48 |
-
<
|
49 |
-
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
</Flex>
|
52 |
-
<Space size={'middle'}>
|
53 |
-
<Button>{t('preview')}</Button>
|
54 |
-
<Button>{t('embedded')}</Button>
|
55 |
-
</Space>
|
56 |
</Card>
|
57 |
<Card title={t('backendServiceApi')}>
|
58 |
<Flex gap={8} vertical>
|
|
|
1 |
+
import CopyToClipboard from '@/components/copy-to-clipboard';
|
2 |
import LineChart from '@/components/line-chart';
|
3 |
+
import { useCreatePublicUrlToken } from '@/hooks/chatHooks';
|
4 |
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
|
5 |
import { IModalProps } from '@/interfaces/common';
|
6 |
import { IDialog, IStats } from '@/interfaces/database/chat';
|
7 |
+
import { ReloadOutlined } from '@ant-design/icons';
|
8 |
import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd';
|
9 |
import { RangePickerProps } from 'antd/es/date-picker';
|
10 |
import dayjs from 'dayjs';
|
11 |
import camelCase from 'lodash/camelCase';
|
12 |
+
import { Link } from 'umi';
|
13 |
import ChatApiKeyModal from '../chat-api-key-modal';
|
14 |
import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks';
|
15 |
import styles from './index.less';
|
|
|
24 |
}: IModalProps<any> & { dialog: IDialog }) => {
|
25 |
const { t } = useTranslate('chat');
|
26 |
const chartList = useSelectChartStatsList();
|
27 |
+
const { urlWithToken, createUrlToken, token } = useCreatePublicUrlToken(
|
28 |
+
dialog.id,
|
29 |
+
visible,
|
30 |
+
);
|
31 |
|
32 |
const {
|
33 |
visible: apiKeyVisible,
|
|
|
53 |
<Card title={dialog.name}>
|
54 |
<Flex gap={8} vertical>
|
55 |
{t('publicUrl')}
|
56 |
+
<Flex className={styles.linkText} gap={10}>
|
57 |
+
<span>{urlWithToken}</span>
|
58 |
+
<CopyToClipboard text={urlWithToken}></CopyToClipboard>
|
59 |
+
<ReloadOutlined onClick={createUrlToken} />
|
60 |
+
</Flex>
|
61 |
+
<Space size={'middle'}>
|
62 |
+
<Button>
|
63 |
+
<Link to={`/chat/share?shared_id=${token}`} target="_blank">
|
64 |
+
{t('preview')}
|
65 |
+
</Link>
|
66 |
+
</Button>
|
67 |
+
<Button>{t('embedded')}</Button>
|
68 |
+
</Space>
|
69 |
</Flex>
|
|
|
|
|
|
|
|
|
70 |
</Card>
|
71 |
<Card title={t('backendServiceApi')}>
|
72 |
<Flex gap={8} vertical>
|
web/src/pages/chat/hooks.ts
CHANGED
@@ -715,6 +715,8 @@ export const useGetSendButtonDisabled = () => {
|
|
715 |
|
716 |
type RangeValue = [Dayjs | null, Dayjs | null] | null;
|
717 |
|
|
|
|
|
718 |
export const useFetchStatsOnMount = (visible: boolean) => {
|
719 |
const fetchStats = useFetchStats();
|
720 |
const [pickerValue, setPickerValue] = useState<RangeValue>([
|
@@ -724,7 +726,10 @@ export const useFetchStatsOnMount = (visible: boolean) => {
|
|
724 |
|
725 |
useEffect(() => {
|
726 |
if (visible && Array.isArray(pickerValue) && pickerValue[0]) {
|
727 |
-
fetchStats({
|
|
|
|
|
|
|
728 |
}
|
729 |
}, [fetchStats, pickerValue, visible]);
|
730 |
|
|
|
715 |
|
716 |
type RangeValue = [Dayjs | null, Dayjs | null] | null;
|
717 |
|
718 |
+
const getDay = (date: Dayjs) => date.format('YYYY-MM-DD');
|
719 |
+
|
720 |
export const useFetchStatsOnMount = (visible: boolean) => {
|
721 |
const fetchStats = useFetchStats();
|
722 |
const [pickerValue, setPickerValue] = useState<RangeValue>([
|
|
|
726 |
|
727 |
useEffect(() => {
|
728 |
if (visible && Array.isArray(pickerValue) && pickerValue[0]) {
|
729 |
+
fetchStats({
|
730 |
+
fromDate: getDay(pickerValue[0]),
|
731 |
+
toDate: getDay(pickerValue[1] ?? dayjs()),
|
732 |
+
});
|
733 |
}
|
734 |
}, [fetchStats, pickerValue, visible]);
|
735 |
|
web/src/pages/chat/markdown-content/index.less
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.referencePopoverWrapper {
|
2 |
+
max-width: 50vw;
|
3 |
+
}
|
4 |
+
|
5 |
+
.referenceChunkImage {
|
6 |
+
width: 10vw;
|
7 |
+
object-fit: contain;
|
8 |
+
}
|
9 |
+
|
10 |
+
.referenceImagePreview {
|
11 |
+
max-width: 45vw;
|
12 |
+
max-height: 45vh;
|
13 |
+
}
|
14 |
+
.chunkContentText {
|
15 |
+
.chunkText;
|
16 |
+
max-height: 45vh;
|
17 |
+
overflow-y: auto;
|
18 |
+
}
|
19 |
+
.documentLink {
|
20 |
+
padding: 0;
|
21 |
+
}
|
22 |
+
|
23 |
+
.referenceIcon {
|
24 |
+
padding: 0 6px;
|
25 |
+
}
|
web/src/pages/chat/markdown-content/index.tsx
ADDED
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Image from '@/components/image';
|
2 |
+
import SvgIcon from '@/components/svg-icon';
|
3 |
+
import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
|
4 |
+
import { IReference } from '@/interfaces/database/chat';
|
5 |
+
import { IChunk } from '@/interfaces/database/knowledge';
|
6 |
+
import { getExtension } from '@/utils/documentUtils';
|
7 |
+
import { InfoCircleOutlined } from '@ant-design/icons';
|
8 |
+
import { Button, Flex, Popover, Space } from 'antd';
|
9 |
+
import { useCallback } from 'react';
|
10 |
+
import Markdown from 'react-markdown';
|
11 |
+
import reactStringReplace from 'react-string-replace';
|
12 |
+
import SyntaxHighlighter from 'react-syntax-highlighter';
|
13 |
+
import remarkGfm from 'remark-gfm';
|
14 |
+
import { visitParents } from 'unist-util-visit-parents';
|
15 |
+
|
16 |
+
import styles from './index.less';
|
17 |
+
|
18 |
+
const reg = /(#{2}\d+\${2})/g;
|
19 |
+
|
20 |
+
const getChunkIndex = (match: string) => Number(match.slice(2, -2));
|
21 |
+
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
22 |
+
const MarkdownContent = ({
|
23 |
+
reference,
|
24 |
+
clickDocumentButton,
|
25 |
+
content,
|
26 |
+
}: {
|
27 |
+
content: string;
|
28 |
+
reference: IReference;
|
29 |
+
clickDocumentButton: (documentId: string, chunk: IChunk) => void;
|
30 |
+
}) => {
|
31 |
+
const fileThumbnails = useSelectFileThumbnails();
|
32 |
+
|
33 |
+
const handleDocumentButtonClick = useCallback(
|
34 |
+
(documentId: string, chunk: IChunk, isPdf: boolean) => () => {
|
35 |
+
if (!isPdf) {
|
36 |
+
return;
|
37 |
+
}
|
38 |
+
clickDocumentButton(documentId, chunk);
|
39 |
+
},
|
40 |
+
[clickDocumentButton],
|
41 |
+
);
|
42 |
+
|
43 |
+
const rehypeWrapReference = () => {
|
44 |
+
return function wrapTextTransform(tree: any) {
|
45 |
+
visitParents(tree, 'text', (node, ancestors) => {
|
46 |
+
const latestAncestor = ancestors.at(-1);
|
47 |
+
if (
|
48 |
+
latestAncestor.tagName !== 'custom-typography' &&
|
49 |
+
latestAncestor.tagName !== 'code'
|
50 |
+
) {
|
51 |
+
node.type = 'element';
|
52 |
+
node.tagName = 'custom-typography';
|
53 |
+
node.properties = {};
|
54 |
+
node.children = [{ type: 'text', value: node.value }];
|
55 |
+
}
|
56 |
+
});
|
57 |
+
};
|
58 |
+
};
|
59 |
+
|
60 |
+
const getPopoverContent = useCallback(
|
61 |
+
(chunkIndex: number) => {
|
62 |
+
const chunks = reference?.chunks ?? [];
|
63 |
+
const chunkItem = chunks[chunkIndex];
|
64 |
+
const document = reference?.doc_aggs.find(
|
65 |
+
(x) => x?.doc_id === chunkItem?.doc_id,
|
66 |
+
);
|
67 |
+
const documentId = document?.doc_id;
|
68 |
+
const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
|
69 |
+
const fileExtension = documentId ? getExtension(document?.doc_name) : '';
|
70 |
+
const imageId = chunkItem?.img_id;
|
71 |
+
return (
|
72 |
+
<Flex
|
73 |
+
key={chunkItem?.chunk_id}
|
74 |
+
gap={10}
|
75 |
+
className={styles.referencePopoverWrapper}
|
76 |
+
>
|
77 |
+
{imageId && (
|
78 |
+
<Popover
|
79 |
+
placement="left"
|
80 |
+
content={
|
81 |
+
<Image
|
82 |
+
id={imageId}
|
83 |
+
className={styles.referenceImagePreview}
|
84 |
+
></Image>
|
85 |
+
}
|
86 |
+
>
|
87 |
+
<Image
|
88 |
+
id={imageId}
|
89 |
+
className={styles.referenceChunkImage}
|
90 |
+
></Image>
|
91 |
+
</Popover>
|
92 |
+
)}
|
93 |
+
<Space direction={'vertical'}>
|
94 |
+
<div
|
95 |
+
dangerouslySetInnerHTML={{
|
96 |
+
__html: chunkItem?.content_with_weight,
|
97 |
+
}}
|
98 |
+
className={styles.chunkContentText}
|
99 |
+
></div>
|
100 |
+
{documentId && (
|
101 |
+
<Flex gap={'small'}>
|
102 |
+
{fileThumbnail ? (
|
103 |
+
<img src={fileThumbnail} alt="" />
|
104 |
+
) : (
|
105 |
+
<SvgIcon
|
106 |
+
name={`file-icon/${fileExtension}`}
|
107 |
+
width={24}
|
108 |
+
></SvgIcon>
|
109 |
+
)}
|
110 |
+
<Button
|
111 |
+
type="link"
|
112 |
+
className={styles.documentLink}
|
113 |
+
onClick={handleDocumentButtonClick(
|
114 |
+
documentId,
|
115 |
+
chunkItem,
|
116 |
+
fileExtension === 'pdf',
|
117 |
+
)}
|
118 |
+
>
|
119 |
+
{document?.doc_name}
|
120 |
+
</Button>
|
121 |
+
</Flex>
|
122 |
+
)}
|
123 |
+
</Space>
|
124 |
+
</Flex>
|
125 |
+
);
|
126 |
+
},
|
127 |
+
[reference, fileThumbnails, handleDocumentButtonClick],
|
128 |
+
);
|
129 |
+
|
130 |
+
const renderReference = useCallback(
|
131 |
+
(text: string) => {
|
132 |
+
return reactStringReplace(text, reg, (match, i) => {
|
133 |
+
const chunkIndex = getChunkIndex(match);
|
134 |
+
return (
|
135 |
+
<Popover content={getPopoverContent(chunkIndex)}>
|
136 |
+
<InfoCircleOutlined key={i} className={styles.referenceIcon} />
|
137 |
+
</Popover>
|
138 |
+
);
|
139 |
+
});
|
140 |
+
},
|
141 |
+
[getPopoverContent],
|
142 |
+
);
|
143 |
+
|
144 |
+
return (
|
145 |
+
<Markdown
|
146 |
+
rehypePlugins={[rehypeWrapReference]}
|
147 |
+
remarkPlugins={[remarkGfm]}
|
148 |
+
components={
|
149 |
+
{
|
150 |
+
'custom-typography': ({ children }: { children: string }) =>
|
151 |
+
renderReference(children),
|
152 |
+
code(props: any) {
|
153 |
+
const { children, className, node, ...rest } = props;
|
154 |
+
const match = /language-(\w+)/.exec(className || '');
|
155 |
+
return match ? (
|
156 |
+
<SyntaxHighlighter {...rest} PreTag="div" language={match[1]}>
|
157 |
+
{String(children).replace(/\n$/, '')}
|
158 |
+
</SyntaxHighlighter>
|
159 |
+
) : (
|
160 |
+
<code {...rest} className={className}>
|
161 |
+
{children}
|
162 |
+
</code>
|
163 |
+
);
|
164 |
+
},
|
165 |
+
} as any
|
166 |
+
}
|
167 |
+
>
|
168 |
+
{content}
|
169 |
+
</Markdown>
|
170 |
+
);
|
171 |
+
};
|
172 |
+
|
173 |
+
export default MarkdownContent;
|
web/src/pages/chat/model.ts
CHANGED
@@ -158,7 +158,7 @@ const model: DvaModel<ChatModelState> = {
|
|
158 |
}
|
159 |
return data;
|
160 |
},
|
161 |
-
*completeConversation({ payload }, { call
|
162 |
const { data } = yield call(chatService.completeConversation, payload);
|
163 |
// if (data.retcode === 0) {
|
164 |
// yield put({
|
@@ -192,7 +192,7 @@ const model: DvaModel<ChatModelState> = {
|
|
192 |
});
|
193 |
message.success(i18n.t('message.created'));
|
194 |
}
|
195 |
-
return data
|
196 |
},
|
197 |
*listToken({ payload }, { call, put }) {
|
198 |
const { data } = yield call(chatService.listToken, payload);
|
@@ -232,13 +232,13 @@ const model: DvaModel<ChatModelState> = {
|
|
232 |
chatService.createExternalConversation,
|
233 |
payload,
|
234 |
);
|
235 |
-
if (data.retcode === 0) {
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
}
|
241 |
-
return data
|
242 |
},
|
243 |
*getExternalConversation({ payload }, { call }) {
|
244 |
const { data } = yield call(
|
@@ -246,7 +246,7 @@ const model: DvaModel<ChatModelState> = {
|
|
246 |
null,
|
247 |
payload,
|
248 |
);
|
249 |
-
return data
|
250 |
},
|
251 |
*completeExternalConversation({ payload }, { call }) {
|
252 |
const { data } = yield call(
|
|
|
158 |
}
|
159 |
return data;
|
160 |
},
|
161 |
+
*completeConversation({ payload }, { call }) {
|
162 |
const { data } = yield call(chatService.completeConversation, payload);
|
163 |
// if (data.retcode === 0) {
|
164 |
// yield put({
|
|
|
192 |
});
|
193 |
message.success(i18n.t('message.created'));
|
194 |
}
|
195 |
+
return data;
|
196 |
},
|
197 |
*listToken({ payload }, { call, put }) {
|
198 |
const { data } = yield call(chatService.listToken, payload);
|
|
|
232 |
chatService.createExternalConversation,
|
233 |
payload,
|
234 |
);
|
235 |
+
// if (data.retcode === 0) {
|
236 |
+
// yield put({
|
237 |
+
// type: 'getExternalConversation',
|
238 |
+
// payload: data.data.id,
|
239 |
+
// });
|
240 |
+
// }
|
241 |
+
return data;
|
242 |
},
|
243 |
*getExternalConversation({ payload }, { call }) {
|
244 |
const { data } = yield call(
|
|
|
246 |
null,
|
247 |
payload,
|
248 |
);
|
249 |
+
return data;
|
250 |
},
|
251 |
*completeExternalConversation({ payload }, { call }) {
|
252 |
const { data } = yield call(
|
web/src/pages/chat/share/index.less
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.chatWrapper {
|
2 |
+
height: 100%;
|
3 |
+
}
|
4 |
+
|
5 |
+
.chatContainer {
|
6 |
+
padding: 10px;
|
7 |
+
box-sizing: border-box;
|
8 |
+
height: 100%;
|
9 |
+
.messageContainer {
|
10 |
+
overflow-y: auto;
|
11 |
+
padding-right: 6px;
|
12 |
+
}
|
13 |
+
}
|
14 |
+
|
15 |
+
.messageItem {
|
16 |
+
padding: 24px 0;
|
17 |
+
.messageItemSection {
|
18 |
+
display: inline-block;
|
19 |
+
}
|
20 |
+
.messageItemSectionLeft {
|
21 |
+
width: 70%;
|
22 |
+
}
|
23 |
+
.messageItemSectionRight {
|
24 |
+
width: 40%;
|
25 |
+
}
|
26 |
+
.messageItemContent {
|
27 |
+
display: inline-flex;
|
28 |
+
gap: 20px;
|
29 |
+
}
|
30 |
+
.messageItemContentReverse {
|
31 |
+
flex-direction: row-reverse;
|
32 |
+
}
|
33 |
+
.messageText {
|
34 |
+
.chunkText();
|
35 |
+
padding: 0 14px;
|
36 |
+
background-color: rgba(249, 250, 251, 1);
|
37 |
+
word-break: break-all;
|
38 |
+
}
|
39 |
+
.messageEmpty {
|
40 |
+
width: 300px;
|
41 |
+
}
|
42 |
+
}
|
43 |
+
|
44 |
+
.messageItemLeft {
|
45 |
+
text-align: left;
|
46 |
+
}
|
47 |
+
|
48 |
+
.messageItemRight {
|
49 |
+
text-align: right;
|
50 |
+
}
|
web/src/pages/chat/share/index.tsx
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect } from 'react';
|
2 |
+
import {
|
3 |
+
useCreateSharedConversationOnMount,
|
4 |
+
useSelectCurrentSharedConversation,
|
5 |
+
useSendSharedMessage,
|
6 |
+
} from '../shared-hooks';
|
7 |
+
import ChatContainer from './large';
|
8 |
+
|
9 |
+
import styles from './index.less';
|
10 |
+
|
11 |
+
const SharedChat = () => {
|
12 |
+
const { conversationId } = useCreateSharedConversationOnMount();
|
13 |
+
const {
|
14 |
+
currentConversation,
|
15 |
+
addNewestConversation,
|
16 |
+
removeLatestMessage,
|
17 |
+
ref,
|
18 |
+
loading,
|
19 |
+
setCurrentConversation,
|
20 |
+
} = useSelectCurrentSharedConversation(conversationId);
|
21 |
+
|
22 |
+
const {
|
23 |
+
handlePressEnter,
|
24 |
+
handleInputChange,
|
25 |
+
value,
|
26 |
+
loading: sendLoading,
|
27 |
+
} = useSendSharedMessage(
|
28 |
+
currentConversation,
|
29 |
+
addNewestConversation,
|
30 |
+
removeLatestMessage,
|
31 |
+
setCurrentConversation,
|
32 |
+
);
|
33 |
+
|
34 |
+
useEffect(() => {
|
35 |
+
console.info(location.href);
|
36 |
+
}, []);
|
37 |
+
|
38 |
+
return (
|
39 |
+
<div className={styles.chatWrapper}>
|
40 |
+
<ChatContainer
|
41 |
+
value={value}
|
42 |
+
handleInputChange={handleInputChange}
|
43 |
+
handlePressEnter={handlePressEnter}
|
44 |
+
loading={loading}
|
45 |
+
sendLoading={sendLoading}
|
46 |
+
conversation={currentConversation}
|
47 |
+
ref={ref}
|
48 |
+
></ChatContainer>
|
49 |
+
</div>
|
50 |
+
);
|
51 |
+
};
|
52 |
+
|
53 |
+
export default SharedChat;
|
web/src/pages/chat/share/large.tsx
ADDED
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
2 |
+
import { MessageType } from '@/constants/chat';
|
3 |
+
import { useTranslate } from '@/hooks/commonHooks';
|
4 |
+
import { Message } from '@/interfaces/database/chat';
|
5 |
+
import { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd';
|
6 |
+
import classNames from 'classnames';
|
7 |
+
import { useSelectConversationLoading } from '../hooks';
|
8 |
+
|
9 |
+
import React, { ChangeEventHandler, forwardRef } from 'react';
|
10 |
+
import { IClientConversation } from '../interface';
|
11 |
+
import styles from './index.less';
|
12 |
+
import SharedMarkdown from './shared-markdown';
|
13 |
+
|
14 |
+
const MessageItem = ({ item }: { item: Message }) => {
|
15 |
+
const isAssistant = item.role === MessageType.Assistant;
|
16 |
+
|
17 |
+
return (
|
18 |
+
<div
|
19 |
+
className={classNames(styles.messageItem, {
|
20 |
+
[styles.messageItemLeft]: item.role === MessageType.Assistant,
|
21 |
+
[styles.messageItemRight]: item.role === MessageType.User,
|
22 |
+
})}
|
23 |
+
>
|
24 |
+
<section
|
25 |
+
className={classNames(styles.messageItemSection, {
|
26 |
+
[styles.messageItemSectionLeft]: item.role === MessageType.Assistant,
|
27 |
+
[styles.messageItemSectionRight]: item.role === MessageType.User,
|
28 |
+
})}
|
29 |
+
>
|
30 |
+
<div
|
31 |
+
className={classNames(styles.messageItemContent, {
|
32 |
+
[styles.messageItemContentReverse]: item.role === MessageType.User,
|
33 |
+
})}
|
34 |
+
>
|
35 |
+
{item.role === MessageType.User ? (
|
36 |
+
<Avatar
|
37 |
+
size={40}
|
38 |
+
src={
|
39 |
+
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
|
40 |
+
}
|
41 |
+
/>
|
42 |
+
) : (
|
43 |
+
<AssistantIcon></AssistantIcon>
|
44 |
+
)}
|
45 |
+
<Flex vertical gap={8} flex={1}>
|
46 |
+
<b>{isAssistant ? '' : 'You'}</b>
|
47 |
+
<div className={styles.messageText}>
|
48 |
+
{item.content !== '' ? (
|
49 |
+
<SharedMarkdown content={item.content}></SharedMarkdown>
|
50 |
+
) : (
|
51 |
+
<Skeleton active className={styles.messageEmpty} />
|
52 |
+
)}
|
53 |
+
</div>
|
54 |
+
</Flex>
|
55 |
+
</div>
|
56 |
+
</section>
|
57 |
+
</div>
|
58 |
+
);
|
59 |
+
};
|
60 |
+
|
61 |
+
interface IProps {
|
62 |
+
handlePressEnter(): void;
|
63 |
+
handleInputChange: ChangeEventHandler<HTMLInputElement>;
|
64 |
+
value: string;
|
65 |
+
loading: boolean;
|
66 |
+
sendLoading: boolean;
|
67 |
+
conversation: IClientConversation;
|
68 |
+
ref: React.LegacyRef<any>;
|
69 |
+
}
|
70 |
+
|
71 |
+
const ChatContainer = (
|
72 |
+
{
|
73 |
+
handlePressEnter,
|
74 |
+
handleInputChange,
|
75 |
+
value,
|
76 |
+
loading: sendLoading,
|
77 |
+
conversation,
|
78 |
+
}: IProps,
|
79 |
+
ref: React.LegacyRef<any>,
|
80 |
+
) => {
|
81 |
+
const loading = useSelectConversationLoading();
|
82 |
+
const { t } = useTranslate('chat');
|
83 |
+
|
84 |
+
return (
|
85 |
+
<>
|
86 |
+
<Flex flex={1} className={styles.chatContainer} vertical>
|
87 |
+
<Flex flex={1} vertical className={styles.messageContainer}>
|
88 |
+
<div>
|
89 |
+
<Spin spinning={loading}>
|
90 |
+
{conversation?.message?.map((message) => {
|
91 |
+
return (
|
92 |
+
<MessageItem key={message.id} item={message}></MessageItem>
|
93 |
+
);
|
94 |
+
})}
|
95 |
+
</Spin>
|
96 |
+
</div>
|
97 |
+
<div ref={ref} />
|
98 |
+
</Flex>
|
99 |
+
<Input
|
100 |
+
size="large"
|
101 |
+
placeholder={t('sendPlaceholder')}
|
102 |
+
value={value}
|
103 |
+
// disabled={disabled}
|
104 |
+
suffix={
|
105 |
+
<Button
|
106 |
+
type="primary"
|
107 |
+
onClick={handlePressEnter}
|
108 |
+
loading={sendLoading}
|
109 |
+
// disabled={disabled}
|
110 |
+
>
|
111 |
+
{t('send')}
|
112 |
+
</Button>
|
113 |
+
}
|
114 |
+
onPressEnter={handlePressEnter}
|
115 |
+
onChange={handleInputChange}
|
116 |
+
/>
|
117 |
+
</Flex>
|
118 |
+
</>
|
119 |
+
);
|
120 |
+
};
|
121 |
+
|
122 |
+
export default forwardRef(ChatContainer);
|
web/src/pages/chat/share/shared-markdown.tsx
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Markdown from 'react-markdown';
|
2 |
+
import SyntaxHighlighter from 'react-syntax-highlighter';
|
3 |
+
import remarkGfm from 'remark-gfm';
|
4 |
+
|
5 |
+
const SharedMarkdown = ({ content }: { content: string }) => {
|
6 |
+
return (
|
7 |
+
<Markdown
|
8 |
+
remarkPlugins={[remarkGfm]}
|
9 |
+
components={
|
10 |
+
{
|
11 |
+
code(props: any) {
|
12 |
+
const { children, className, node, ...rest } = props;
|
13 |
+
const match = /language-(\w+)/.exec(className || '');
|
14 |
+
return match ? (
|
15 |
+
<SyntaxHighlighter {...rest} PreTag="div" language={match[1]}>
|
16 |
+
{String(children).replace(/\n$/, '')}
|
17 |
+
</SyntaxHighlighter>
|
18 |
+
) : (
|
19 |
+
<code {...rest} className={className}>
|
20 |
+
{children}
|
21 |
+
</code>
|
22 |
+
);
|
23 |
+
},
|
24 |
+
} as any
|
25 |
+
}
|
26 |
+
>
|
27 |
+
{content}
|
28 |
+
</Markdown>
|
29 |
+
);
|
30 |
+
};
|
31 |
+
|
32 |
+
export default SharedMarkdown;
|
web/src/pages/chat/shared-hooks.ts
ADDED
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { MessageType } from '@/constants/chat';
|
2 |
+
import {
|
3 |
+
useCompleteSharedConversation,
|
4 |
+
useCreateSharedConversation,
|
5 |
+
useFetchSharedConversation,
|
6 |
+
} from '@/hooks/chatHooks';
|
7 |
+
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
8 |
+
import omit from 'lodash/omit';
|
9 |
+
import {
|
10 |
+
Dispatch,
|
11 |
+
SetStateAction,
|
12 |
+
useCallback,
|
13 |
+
useEffect,
|
14 |
+
useState,
|
15 |
+
} from 'react';
|
16 |
+
import { useSearchParams } from 'umi';
|
17 |
+
import { v4 as uuid } from 'uuid';
|
18 |
+
import { useHandleMessageInputChange, useScrollToBottom } from './hooks';
|
19 |
+
import { IClientConversation, IMessage } from './interface';
|
20 |
+
|
21 |
+
export const useCreateSharedConversationOnMount = () => {
|
22 |
+
const [currentQueryParameters] = useSearchParams();
|
23 |
+
const [conversationId, setConversationId] = useState('');
|
24 |
+
|
25 |
+
const createConversation = useCreateSharedConversation();
|
26 |
+
const sharedId = currentQueryParameters.get('shared_id');
|
27 |
+
const userId = currentQueryParameters.get('user_id');
|
28 |
+
|
29 |
+
const setConversation = useCallback(async () => {
|
30 |
+
console.info(sharedId);
|
31 |
+
if (sharedId) {
|
32 |
+
const data = await createConversation(userId ?? undefined);
|
33 |
+
const id = data.data?.id;
|
34 |
+
if (id) {
|
35 |
+
setConversationId(id);
|
36 |
+
}
|
37 |
+
}
|
38 |
+
}, [createConversation, sharedId, userId]);
|
39 |
+
|
40 |
+
useEffect(() => {
|
41 |
+
setConversation();
|
42 |
+
}, [setConversation]);
|
43 |
+
|
44 |
+
return { conversationId };
|
45 |
+
};
|
46 |
+
|
47 |
+
export const useSelectCurrentSharedConversation = (conversationId: string) => {
|
48 |
+
const [currentConversation, setCurrentConversation] =
|
49 |
+
useState<IClientConversation>({} as IClientConversation);
|
50 |
+
const fetchConversation = useFetchSharedConversation();
|
51 |
+
const loading = useOneNamespaceEffectsLoading('chatModel', [
|
52 |
+
'getExternalConversation',
|
53 |
+
]);
|
54 |
+
|
55 |
+
const ref = useScrollToBottom(currentConversation);
|
56 |
+
|
57 |
+
const addNewestConversation = useCallback((message: string) => {
|
58 |
+
setCurrentConversation((pre) => {
|
59 |
+
return {
|
60 |
+
...pre,
|
61 |
+
message: [
|
62 |
+
...(pre.message ?? []),
|
63 |
+
{
|
64 |
+
role: MessageType.User,
|
65 |
+
content: message,
|
66 |
+
id: uuid(),
|
67 |
+
} as IMessage,
|
68 |
+
{
|
69 |
+
role: MessageType.Assistant,
|
70 |
+
content: '',
|
71 |
+
id: uuid(),
|
72 |
+
reference: [],
|
73 |
+
} as IMessage,
|
74 |
+
],
|
75 |
+
};
|
76 |
+
});
|
77 |
+
}, []);
|
78 |
+
|
79 |
+
const removeLatestMessage = useCallback(() => {
|
80 |
+
setCurrentConversation((pre) => {
|
81 |
+
const nextMessages = pre.message.slice(0, -2);
|
82 |
+
return {
|
83 |
+
...pre,
|
84 |
+
message: nextMessages,
|
85 |
+
};
|
86 |
+
});
|
87 |
+
}, []);
|
88 |
+
|
89 |
+
const fetchConversationOnMount = useCallback(async () => {
|
90 |
+
if (conversationId) {
|
91 |
+
const data = await fetchConversation(conversationId);
|
92 |
+
if (data.retcode === 0) {
|
93 |
+
setCurrentConversation(data.data);
|
94 |
+
}
|
95 |
+
}
|
96 |
+
}, [conversationId, fetchConversation]);
|
97 |
+
|
98 |
+
useEffect(() => {
|
99 |
+
fetchConversationOnMount();
|
100 |
+
}, [fetchConversationOnMount]);
|
101 |
+
|
102 |
+
return {
|
103 |
+
currentConversation,
|
104 |
+
addNewestConversation,
|
105 |
+
removeLatestMessage,
|
106 |
+
loading,
|
107 |
+
ref,
|
108 |
+
setCurrentConversation,
|
109 |
+
};
|
110 |
+
};
|
111 |
+
|
112 |
+
export const useSendSharedMessage = (
|
113 |
+
conversation: IClientConversation,
|
114 |
+
addNewestConversation: (message: string) => void,
|
115 |
+
removeLatestMessage: () => void,
|
116 |
+
setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>,
|
117 |
+
) => {
|
118 |
+
const conversationId = conversation.id;
|
119 |
+
const loading = useOneNamespaceEffectsLoading('chatModel', [
|
120 |
+
'completeExternalConversation',
|
121 |
+
]);
|
122 |
+
const setConversation = useCreateSharedConversation();
|
123 |
+
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
|
124 |
+
|
125 |
+
const fetchConversation = useFetchSharedConversation();
|
126 |
+
const completeConversation = useCompleteSharedConversation();
|
127 |
+
|
128 |
+
const sendMessage = useCallback(
|
129 |
+
async (message: string, id?: string) => {
|
130 |
+
const retcode = await completeConversation({
|
131 |
+
conversation_id: id ?? conversationId,
|
132 |
+
messages: [
|
133 |
+
...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
|
134 |
+
{
|
135 |
+
role: MessageType.User,
|
136 |
+
content: message,
|
137 |
+
},
|
138 |
+
],
|
139 |
+
});
|
140 |
+
|
141 |
+
if (retcode === 0) {
|
142 |
+
const data = await fetchConversation(conversationId);
|
143 |
+
if (data.retcode === 0) {
|
144 |
+
setCurrentConversation(data.data);
|
145 |
+
}
|
146 |
+
} else {
|
147 |
+
// cancel loading
|
148 |
+
setValue(message);
|
149 |
+
removeLatestMessage();
|
150 |
+
}
|
151 |
+
},
|
152 |
+
[
|
153 |
+
conversationId,
|
154 |
+
conversation?.message,
|
155 |
+
fetchConversation,
|
156 |
+
removeLatestMessage,
|
157 |
+
setValue,
|
158 |
+
completeConversation,
|
159 |
+
setCurrentConversation,
|
160 |
+
],
|
161 |
+
);
|
162 |
+
|
163 |
+
const handleSendMessage = useCallback(
|
164 |
+
async (message: string) => {
|
165 |
+
if (conversationId !== '') {
|
166 |
+
sendMessage(message);
|
167 |
+
} else {
|
168 |
+
const data = await setConversation('user id');
|
169 |
+
if (data.retcode === 0) {
|
170 |
+
const id = data.data.id;
|
171 |
+
sendMessage(message, id);
|
172 |
+
}
|
173 |
+
}
|
174 |
+
},
|
175 |
+
[conversationId, setConversation, sendMessage],
|
176 |
+
);
|
177 |
+
|
178 |
+
const handlePressEnter = () => {
|
179 |
+
if (!loading) {
|
180 |
+
setValue('');
|
181 |
+
addNewestConversation(value);
|
182 |
+
handleSendMessage(value.trim());
|
183 |
+
}
|
184 |
+
};
|
185 |
+
|
186 |
+
return {
|
187 |
+
handlePressEnter,
|
188 |
+
handleInputChange,
|
189 |
+
value,
|
190 |
+
loading,
|
191 |
+
};
|
192 |
+
};
|
web/src/routes.ts
CHANGED
@@ -4,6 +4,11 @@ const routes = [
|
|
4 |
component: '@/pages/login',
|
5 |
layout: false,
|
6 |
},
|
|
|
|
|
|
|
|
|
|
|
7 |
{
|
8 |
path: '/',
|
9 |
component: '@/layouts',
|
|
|
4 |
component: '@/pages/login',
|
5 |
layout: false,
|
6 |
},
|
7 |
+
{
|
8 |
+
path: '/chat/share',
|
9 |
+
component: '@/pages/chat/share',
|
10 |
+
layout: false,
|
11 |
+
},
|
12 |
{
|
13 |
path: '/',
|
14 |
component: '@/layouts',
|
web/src/services/chatService.ts
CHANGED
@@ -76,7 +76,7 @@ const methods = {
|
|
76 |
},
|
77 |
createExternalConversation: {
|
78 |
url: createExternalConversation,
|
79 |
-
method: '
|
80 |
},
|
81 |
getExternalConversation: {
|
82 |
url: getExternalConversation,
|
|
|
76 |
},
|
77 |
createExternalConversation: {
|
78 |
url: createExternalConversation,
|
79 |
+
method: 'get',
|
80 |
},
|
81 |
getExternalConversation: {
|
82 |
url: getExternalConversation,
|
web/src/utils/commonUtil.ts
CHANGED
@@ -15,3 +15,8 @@ export const convertTheKeysOfTheObjectToSnake = (data: unknown) => {
|
|
15 |
}
|
16 |
return data;
|
17 |
};
|
|
|
|
|
|
|
|
|
|
|
|
15 |
}
|
16 |
return data;
|
17 |
};
|
18 |
+
|
19 |
+
export const getSearchValue = (key: string) => {
|
20 |
+
const params = new URL(document.location as any).searchParams;
|
21 |
+
return params.get(key);
|
22 |
+
};
|
web/src/utils/request.ts
CHANGED
@@ -4,7 +4,7 @@ import authorizationUtil from '@/utils/authorizationUtil';
|
|
4 |
import { message, notification } from 'antd';
|
5 |
import { history } from 'umi';
|
6 |
import { RequestMethod, extend } from 'umi-request';
|
7 |
-
import { convertTheKeysOfTheObjectToSnake } from './commonUtil';
|
8 |
|
9 |
const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message
|
10 |
|
@@ -87,7 +87,10 @@ const request: RequestMethod = extend({
|
|
87 |
});
|
88 |
|
89 |
request.interceptors.request.use((url: string, options: any) => {
|
90 |
-
const
|
|
|
|
|
|
|
91 |
const data = convertTheKeysOfTheObjectToSnake(options.data);
|
92 |
const params = convertTheKeysOfTheObjectToSnake(options.params);
|
93 |
|
|
|
4 |
import { message, notification } from 'antd';
|
5 |
import { history } from 'umi';
|
6 |
import { RequestMethod, extend } from 'umi-request';
|
7 |
+
import { convertTheKeysOfTheObjectToSnake, getSearchValue } from './commonUtil';
|
8 |
|
9 |
const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message
|
10 |
|
|
|
87 |
});
|
88 |
|
89 |
request.interceptors.request.use((url: string, options: any) => {
|
90 |
+
const sharedId = getSearchValue('shared_id');
|
91 |
+
const authorization = sharedId
|
92 |
+
? 'Bearer ' + sharedId
|
93 |
+
: authorizationUtil.getAuthorization();
|
94 |
const data = convertTheKeysOfTheObjectToSnake(options.data);
|
95 |
const params = convertTheKeysOfTheObjectToSnake(options.params);
|
96 |
|