balibabu commited on
Commit
cb33b9e
·
1 Parent(s): 028fe40

feat: Support for conversational streaming (#809)

Browse files

### What problem does this PR solve?

feat: Support for conversational streaming
#709

### Type of change


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

web/.env CHANGED
@@ -0,0 +1 @@
 
 
1
+ PORT=9222
web/package-lock.json CHANGED
@@ -15,6 +15,7 @@
15
  "axios": "^1.6.3",
16
  "classnames": "^2.5.1",
17
  "dayjs": "^1.11.10",
 
18
  "i18next": "^23.7.16",
19
  "js-base64": "^3.7.5",
20
  "jsencrypt": "^3.3.2",
@@ -10206,6 +10207,14 @@
10206
  "node": ">=0.8.x"
10207
  }
10208
  },
 
 
 
 
 
 
 
 
10209
  "node_modules/evp_bytestokey": {
10210
  "version": "1.0.3",
10211
  "resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
 
15
  "axios": "^1.6.3",
16
  "classnames": "^2.5.1",
17
  "dayjs": "^1.11.10",
18
+ "eventsource-parser": "^1.1.2",
19
  "i18next": "^23.7.16",
20
  "js-base64": "^3.7.5",
21
  "jsencrypt": "^3.3.2",
 
10207
  "node": ">=0.8.x"
10208
  }
10209
  },
10210
+ "node_modules/eventsource-parser": {
10211
+ "version": "1.1.2",
10212
+ "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
10213
+ "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
10214
+ "engines": {
10215
+ "node": ">=14.18"
10216
+ }
10217
+ },
10218
  "node_modules/evp_bytestokey": {
10219
  "version": "1.0.3",
10220
  "resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
web/package.json CHANGED
@@ -3,7 +3,7 @@
3
  "author": "zhaofengchao <[email protected]>",
4
  "scripts": {
5
  "build": "umi build",
6
- "dev": "cross-env PORT=9200 UMI_DEV_SERVER_COMPRESS=none umi dev",
7
  "postinstall": "umi setup",
8
  "lint": "umi lint --eslint-only",
9
  "setup": "umi setup",
@@ -19,6 +19,7 @@
19
  "axios": "^1.6.3",
20
  "classnames": "^2.5.1",
21
  "dayjs": "^1.11.10",
 
22
  "i18next": "^23.7.16",
23
  "js-base64": "^3.7.5",
24
  "jsencrypt": "^3.3.2",
 
3
  "author": "zhaofengchao <[email protected]>",
4
  "scripts": {
5
  "build": "umi build",
6
+ "dev": "cross-env UMI_DEV_SERVER_COMPRESS=none umi dev",
7
  "postinstall": "umi setup",
8
  "lint": "umi lint --eslint-only",
9
  "setup": "umi setup",
 
19
  "axios": "^1.6.3",
20
  "classnames": "^2.5.1",
21
  "dayjs": "^1.11.10",
22
+ "eventsource-parser": "^1.1.2",
23
  "i18next": "^23.7.16",
24
  "js-base64": "^3.7.5",
25
  "jsencrypt": "^3.3.2",
web/src/components/new-document-link.tsx CHANGED
@@ -18,7 +18,7 @@ const NewDocumentLink = ({
18
  onClick={!preventDefault ? undefined : (e) => e.preventDefault()}
19
  href={link}
20
  rel="noreferrer"
21
- style={{ color }}
22
  >
23
  {children}
24
  </a>
 
18
  onClick={!preventDefault ? undefined : (e) => e.preventDefault()}
19
  href={link}
20
  rel="noreferrer"
21
+ style={{ color, wordBreak: 'break-all' }}
22
  >
23
  {children}
24
  </a>
web/src/hooks/chatHooks.ts CHANGED
@@ -154,6 +154,9 @@ export const useRemoveConversation = () => {
154
  return removeConversation;
155
  };
156
 
 
 
 
157
  export const useCompleteConversation = () => {
158
  const dispatch = useDispatch();
159
 
@@ -283,20 +286,4 @@ export const useFetchSharedConversation = () => {
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
  //#endregion
 
154
  return removeConversation;
155
  };
156
 
157
+ /*
158
+ @deprecated
159
+ */
160
  export const useCompleteConversation = () => {
161
  const dispatch = useDispatch();
162
 
 
286
  return fetchSharedConversation;
287
  };
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  //#endregion
web/src/hooks/logicHooks.ts CHANGED
@@ -1,13 +1,14 @@
1
  import { Authorization } from '@/constants/authorization';
2
  import { LanguageTranslationMap } from '@/constants/common';
3
  import { Pagination } from '@/interfaces/common';
 
4
  import { IKnowledgeFile } from '@/interfaces/database/knowledge';
5
  import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
6
  import api from '@/utils/api';
7
- import authorizationUtil from '@/utils/authorizationUtil';
8
- import { getSearchValue } from '@/utils/commonUtil';
9
  import { PaginationProps } from 'antd';
10
  import axios from 'axios';
 
11
  import { useCallback, useEffect, useMemo, useState } from 'react';
12
  import { useTranslation } from 'react-i18next';
13
  import { useDispatch } from 'umi';
@@ -138,62 +139,60 @@ export const useFetchAppConf = () => {
138
  return appConf;
139
  };
140
 
141
- export const useConnectWithSse = (url: string) => {
142
- const [content, setContent] = useState<string>('');
143
-
144
- const connect = useCallback(() => {
145
- const source = new EventSource(
146
- url || '/sse/createSseEmitter?clientId=123456',
147
- );
148
-
149
- source.onopen = function () {
150
- console.log('Connection to the server was opened.');
151
- };
152
-
153
- source.onmessage = function (event: any) {
154
- setContent(event.data);
155
- };
156
-
157
- source.onerror = function (error) {
158
- console.error('Error occurred:', error);
159
- };
160
- }, [url]);
161
-
162
- return { connect, content };
163
- };
164
 
165
- export const useConnectWithSseNext = () => {
166
- const [content, setContent] = useState<string>('');
167
- const sharedId = getSearchValue('shared_id');
168
- const authorization = sharedId
169
- ? 'Bearer ' + sharedId
170
- : authorizationUtil.getAuthorization();
171
  const send = useCallback(
172
  async (body: any) => {
173
- const response = await fetch(api.completeConversation, {
174
- method: 'POST',
175
- headers: {
176
- [Authorization]: authorization,
177
- 'Content-Type': 'application/json',
178
- },
179
- body: JSON.stringify(body),
180
- });
181
- const reader = response?.body
182
- ?.pipeThrough(new TextDecoderStream())
183
- .getReader();
184
-
185
- // const reader = response.body.getReader();
186
-
187
- while (true) {
188
- const { value, done } = await reader?.read();
189
- console.log('Received', value);
190
- setContent(value);
191
- if (done) break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
- return response;
194
  },
195
- [authorization],
196
  );
197
 
198
- return { send, content };
199
  };
 
1
  import { Authorization } from '@/constants/authorization';
2
  import { LanguageTranslationMap } from '@/constants/common';
3
  import { Pagination } from '@/interfaces/common';
4
+ import { IAnswer } from '@/interfaces/database/chat';
5
  import { IKnowledgeFile } from '@/interfaces/database/knowledge';
6
  import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
7
  import api from '@/utils/api';
8
+ import { getAuthorization } from '@/utils/authorizationUtil';
 
9
  import { PaginationProps } from 'antd';
10
  import axios from 'axios';
11
+ import { EventSourceParserStream } from 'eventsource-parser/stream';
12
  import { useCallback, useEffect, useMemo, useState } from 'react';
13
  import { useTranslation } from 'react-i18next';
14
  import { useDispatch } from 'umi';
 
139
  return appConf;
140
  };
141
 
142
+ export const useSendMessageWithSse = (
143
+ url: string = api.completeConversation,
144
+ ) => {
145
+ const [answer, setAnswer] = useState<IAnswer>({} as IAnswer);
146
+ const [done, setDone] = useState(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
 
 
 
 
 
 
148
  const send = useCallback(
149
  async (body: any) => {
150
+ try {
151
+ setDone(false);
152
+ const response = await fetch(url, {
153
+ method: 'POST',
154
+ headers: {
155
+ [Authorization]: getAuthorization(),
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ body: JSON.stringify(body),
159
+ });
160
+
161
+ const reader = response?.body
162
+ ?.pipeThrough(new TextDecoderStream())
163
+ .pipeThrough(new EventSourceParserStream())
164
+ .getReader();
165
+
166
+ while (true) {
167
+ const x = await reader?.read();
168
+ if (x) {
169
+ const { done, value } = x;
170
+ try {
171
+ const val = JSON.parse(value?.data || '');
172
+ const d = val?.data;
173
+ if (typeof d !== 'boolean') {
174
+ console.info('data:', d);
175
+ setAnswer(d);
176
+ }
177
+ } catch (e) {
178
+ console.warn(e);
179
+ }
180
+ if (done) {
181
+ console.info('done');
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ console.info('done?');
187
+ setDone(true);
188
+ return response;
189
+ } catch (e) {
190
+ setDone(true);
191
+ console.warn(e);
192
  }
 
193
  },
194
+ [url],
195
  );
196
 
197
+ return { send, answer, done };
198
  };
web/src/interfaces/database/chat.ts CHANGED
@@ -72,6 +72,11 @@ export interface IReference {
72
  total: number;
73
  }
74
 
 
 
 
 
 
75
  export interface Docagg {
76
  count: number;
77
  doc_id: string;
 
72
  total: number;
73
  }
74
 
75
+ export interface IAnswer {
76
+ answer: string;
77
+ reference: IReference;
78
+ }
79
+
80
  export interface Docagg {
81
  count: number;
82
  doc_id: string;
web/src/locales/en.ts CHANGED
@@ -25,6 +25,7 @@ export default {
25
  comingSoon: 'Coming Soon',
26
  download: 'Download',
27
  close: 'Close',
 
28
  },
29
  login: {
30
  login: 'Sign in',
@@ -381,6 +382,7 @@ export default {
381
  partialTitle: 'Partial Embed',
382
  extensionTitle: 'Chrome Extension',
383
  tokenError: 'Please create API Token first!',
 
384
  },
385
  setting: {
386
  profile: 'Profile',
 
25
  comingSoon: 'Coming Soon',
26
  download: 'Download',
27
  close: 'Close',
28
+ preview: 'Preview',
29
  },
30
  login: {
31
  login: 'Sign in',
 
382
  partialTitle: 'Partial Embed',
383
  extensionTitle: 'Chrome Extension',
384
  tokenError: 'Please create API Token first!',
385
+ searching: 'searching...',
386
  },
387
  setting: {
388
  profile: 'Profile',
web/src/locales/zh-traditional.ts CHANGED
@@ -25,6 +25,7 @@ export default {
25
  comingSoon: '即將推出',
26
  download: '下載',
27
  close: '关闭',
 
28
  },
29
  login: {
30
  login: '登入',
@@ -352,6 +353,7 @@ export default {
352
  partialTitle: '部分嵌入',
353
  extensionTitle: 'Chrome 插件',
354
  tokenError: '請先創建 Api Token!',
 
355
  },
356
  setting: {
357
  profile: '概述',
 
25
  comingSoon: '即將推出',
26
  download: '下載',
27
  close: '关闭',
28
+ preview: '預覽',
29
  },
30
  login: {
31
  login: '登入',
 
353
  partialTitle: '部分嵌入',
354
  extensionTitle: 'Chrome 插件',
355
  tokenError: '請先創建 Api Token!',
356
+ searching: '搜索中',
357
  },
358
  setting: {
359
  profile: '概述',
web/src/locales/zh.ts CHANGED
@@ -25,6 +25,7 @@ export default {
25
  comingSoon: '即将推出',
26
  download: '下载',
27
  close: '关闭',
 
28
  },
29
  login: {
30
  login: '登录',
@@ -369,6 +370,7 @@ export default {
369
  partialTitle: '部分嵌入',
370
  extensionTitle: 'Chrome 插件',
371
  tokenError: '请先创建 Api Token!',
 
372
  },
373
  setting: {
374
  profile: '概要',
 
25
  comingSoon: '即将推出',
26
  download: '下载',
27
  close: '关闭',
28
+ preview: '预览',
29
  },
30
  login: {
31
  login: '登录',
 
370
  partialTitle: '部分嵌入',
371
  extensionTitle: 'Chrome 插件',
372
  tokenError: '请先创建 Api Token!',
373
+ searching: '搜索中',
374
  },
375
  setting: {
376
  profile: '概要',
web/src/pages/chat/chat-container/index.tsx CHANGED
@@ -6,16 +6,7 @@ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
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,
12
- Drawer,
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 {
@@ -32,20 +23,24 @@ import SvgIcon from '@/components/svg-icon';
32
  import { useTranslate } from '@/hooks/commonHooks';
33
  import { useGetDocumentUrl } from '@/hooks/documentHooks';
34
  import { getExtension, isPdf } from '@/utils/documentUtils';
 
35
  import styles from './index.less';
36
 
37
  const MessageItem = ({
38
  item,
39
  reference,
 
40
  clickDocumentButton,
41
  }: {
42
  item: Message;
43
  reference: IReference;
 
44
  clickDocumentButton: (documentId: string, chunk: IChunk) => void;
45
  }) => {
46
  const userInfo = useSelectUserInfo();
47
  const fileThumbnails = useSelectFileThumbnails();
48
  const getDocumentUrl = useGetDocumentUrl();
 
49
 
50
  const isAssistant = item.role === MessageType.Assistant;
51
 
@@ -53,6 +48,14 @@ const MessageItem = ({
53
  return reference?.doc_aggs ?? [];
54
  }, [reference?.doc_aggs]);
55
 
 
 
 
 
 
 
 
 
56
  return (
57
  <div
58
  className={classNames(styles.messageItem, {
@@ -85,15 +88,11 @@ const MessageItem = ({
85
  <Flex vertical gap={8} flex={1}>
86
  <b>{isAssistant ? '' : userInfo.nickname}</b>
87
  <div className={styles.messageText}>
88
- {item.content !== '' ? (
89
- <MarkdownContent
90
- content={item.content}
91
- reference={reference}
92
- clickDocumentButton={clickDocumentButton}
93
- ></MarkdownContent>
94
- ) : (
95
- <Skeleton active className={styles.messageEmpty} />
96
- )}
97
  </div>
98
  {isAssistant && referenceDocumentList.length > 0 && (
99
  <List
@@ -139,13 +138,19 @@ const ChatContainer = () => {
139
  currentConversation: conversation,
140
  addNewestConversation,
141
  removeLatestMessage,
 
142
  } = useFetchConversationOnMount();
143
  const {
144
  handleInputChange,
145
  handlePressEnter,
146
  value,
147
  loading: sendLoading,
148
- } = useSendMessage(conversation, addNewestConversation, removeLatestMessage);
 
 
 
 
 
149
  const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
150
  useClickDrawer();
151
  const disabled = useGetSendButtonDisabled();
@@ -159,19 +164,17 @@ const ChatContainer = () => {
159
  <Flex flex={1} vertical className={styles.messageContainer}>
160
  <div>
161
  <Spin spinning={loading}>
162
- {conversation?.message?.map((message) => {
163
- const assistantMessages = conversation?.message
164
- ?.filter((x) => x.role === MessageType.Assistant)
165
- .slice(1);
166
- const referenceIndex = assistantMessages.findIndex(
167
- (x) => x.id === message.id,
168
- );
169
- const reference = conversation.reference[referenceIndex];
170
  return (
171
  <MessageItem
 
 
 
 
 
172
  key={message.id}
173
  item={message}
174
- reference={reference}
175
  clickDocumentButton={clickDocumentButton}
176
  ></MessageItem>
177
  );
 
6
  import { useSelectUserInfo } from '@/hooks/userSettingHook';
7
  import { IReference, Message } from '@/interfaces/database/chat';
8
  import { IChunk } from '@/interfaces/database/knowledge';
9
+ import { Avatar, Button, Drawer, Flex, Input, List, Spin } from 'antd';
 
 
 
 
 
 
 
 
 
10
  import classNames from 'classnames';
11
  import { useMemo } from 'react';
12
  import {
 
23
  import { useTranslate } from '@/hooks/commonHooks';
24
  import { useGetDocumentUrl } from '@/hooks/documentHooks';
25
  import { getExtension, isPdf } from '@/utils/documentUtils';
26
+ import { buildMessageItemReference } from '../utils';
27
  import styles from './index.less';
28
 
29
  const MessageItem = ({
30
  item,
31
  reference,
32
+ loading = false,
33
  clickDocumentButton,
34
  }: {
35
  item: Message;
36
  reference: IReference;
37
+ loading?: boolean;
38
  clickDocumentButton: (documentId: string, chunk: IChunk) => void;
39
  }) => {
40
  const userInfo = useSelectUserInfo();
41
  const fileThumbnails = useSelectFileThumbnails();
42
  const getDocumentUrl = useGetDocumentUrl();
43
+ const { t } = useTranslate('chat');
44
 
45
  const isAssistant = item.role === MessageType.Assistant;
46
 
 
48
  return reference?.doc_aggs ?? [];
49
  }, [reference?.doc_aggs]);
50
 
51
+ const content = useMemo(() => {
52
+ let text = item.content;
53
+ if (text === '') {
54
+ text = t('searching');
55
+ }
56
+ return loading ? text?.concat('~~2$$') : text;
57
+ }, [item.content, loading, t]);
58
+
59
  return (
60
  <div
61
  className={classNames(styles.messageItem, {
 
88
  <Flex vertical gap={8} flex={1}>
89
  <b>{isAssistant ? '' : userInfo.nickname}</b>
90
  <div className={styles.messageText}>
91
+ <MarkdownContent
92
+ content={content}
93
+ reference={reference}
94
+ clickDocumentButton={clickDocumentButton}
95
+ ></MarkdownContent>
 
 
 
 
96
  </div>
97
  {isAssistant && referenceDocumentList.length > 0 && (
98
  <List
 
138
  currentConversation: conversation,
139
  addNewestConversation,
140
  removeLatestMessage,
141
+ addNewestAnswer,
142
  } = useFetchConversationOnMount();
143
  const {
144
  handleInputChange,
145
  handlePressEnter,
146
  value,
147
  loading: sendLoading,
148
+ } = useSendMessage(
149
+ conversation,
150
+ addNewestConversation,
151
+ removeLatestMessage,
152
+ addNewestAnswer,
153
+ );
154
  const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
155
  useClickDrawer();
156
  const disabled = useGetSendButtonDisabled();
 
164
  <Flex flex={1} vertical className={styles.messageContainer}>
165
  <div>
166
  <Spin spinning={loading}>
167
+ {conversation?.message?.map((message, i) => {
 
 
 
 
 
 
 
168
  return (
169
  <MessageItem
170
+ loading={
171
+ message.role === MessageType.Assistant &&
172
+ sendLoading &&
173
+ conversation?.message.length - 1 === i
174
+ }
175
  key={message.id}
176
  item={message}
177
+ reference={buildMessageItemReference(conversation, message)}
178
  clickDocumentButton={clickDocumentButton}
179
  ></MessageItem>
180
  );
web/src/pages/chat/hooks.ts CHANGED
@@ -1,7 +1,6 @@
1
  import { MessageType } from '@/constants/chat';
2
  import { fileIconMap } from '@/constants/common';
3
  import {
4
- useCompleteConversation,
5
  useCreateToken,
6
  useFetchConversation,
7
  useFetchConversationList,
@@ -24,8 +23,14 @@ import {
24
  useShowDeleteConfirm,
25
  useTranslate,
26
  } from '@/hooks/commonHooks';
 
27
  import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
28
- import { IConversation, IDialog, IStats } from '@/interfaces/database/chat';
 
 
 
 
 
29
  import { IChunk } from '@/interfaces/database/knowledge';
30
  import { getFileExtension } from '@/utils';
31
  import { message } from 'antd';
@@ -380,31 +385,56 @@ export const useSelectCurrentConversation = () => {
380
  const dialog = useSelectCurrentDialog();
381
  const { conversationId, dialogId } = useGetChatSearchParams();
382
 
383
- const addNewestConversation = useCallback((message: string) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  setCurrentConversation((pre) => {
385
- return {
386
- ...pre,
387
- message: [
388
- ...pre.message,
389
- {
390
- role: MessageType.User,
391
- content: message,
392
- id: uuid(),
393
- } as IMessage,
394
- {
395
- role: MessageType.Assistant,
396
- content: '',
397
- id: uuid(),
398
- reference: [],
399
- } as IMessage,
400
- ],
401
- };
402
  });
403
  }, []);
404
 
405
  const removeLatestMessage = useCallback(() => {
 
406
  setCurrentConversation((pre) => {
407
- const nextMessages = pre.message.slice(0, -2);
408
  return {
409
  ...pre,
410
  message: nextMessages,
@@ -441,7 +471,12 @@ export const useSelectCurrentConversation = () => {
441
  }
442
  }, [conversation, conversationId]);
443
 
444
- return { currentConversation, addNewestConversation, removeLatestMessage };
 
 
 
 
 
445
  };
446
 
447
  export const useScrollToBottom = (currentConversation: IClientConversation) => {
@@ -464,8 +499,12 @@ export const useScrollToBottom = (currentConversation: IClientConversation) => {
464
  export const useFetchConversationOnMount = () => {
465
  const { conversationId } = useGetChatSearchParams();
466
  const fetchConversation = useFetchConversation();
467
- const { currentConversation, addNewestConversation, removeLatestMessage } =
468
- useSelectCurrentConversation();
 
 
 
 
469
  const ref = useScrollToBottom(currentConversation);
470
 
471
  const fetchConversationOnMount = useCallback(() => {
@@ -483,6 +522,7 @@ export const useFetchConversationOnMount = () => {
483
  addNewestConversation,
484
  ref,
485
  removeLatestMessage,
 
486
  };
487
  };
488
 
@@ -504,25 +544,22 @@ export const useHandleMessageInputChange = () => {
504
 
505
  export const useSendMessage = (
506
  conversation: IClientConversation,
507
- addNewestConversation: (message: string) => void,
508
  removeLatestMessage: () => void,
 
509
  ) => {
510
- const loading = useOneNamespaceEffectsLoading('chatModel', [
511
- 'completeConversation',
512
- ]);
513
  const { setConversation } = useSetConversation();
514
  const { conversationId } = useGetChatSearchParams();
515
  const { handleInputChange, value, setValue } = useHandleMessageInputChange();
516
 
517
  const fetchConversation = useFetchConversation();
518
- const completeConversation = useCompleteConversation();
519
 
520
  const { handleClickConversation } = useClickConversationCard();
521
- // const { send } = useConnectWithSseNext();
522
 
523
  const sendMessage = useCallback(
524
  async (message: string, id?: string) => {
525
- const retcode = await completeConversation({
526
  conversation_id: id ?? conversationId,
527
  messages: [
528
  ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
@@ -533,27 +570,33 @@ export const useSendMessage = (
533
  ],
534
  });
535
 
536
- if (retcode === 0) {
537
  if (id) {
 
538
  // new conversation
539
  handleClickConversation(id);
540
  } else {
541
- fetchConversation(conversationId);
 
542
  }
543
  } else {
 
 
544
  // cancel loading
545
  setValue(message);
 
546
  removeLatestMessage();
547
  }
 
548
  },
549
  [
550
  conversation?.message,
551
  conversationId,
552
- fetchConversation,
553
  handleClickConversation,
554
  removeLatestMessage,
555
  setValue,
556
- completeConversation,
557
  ],
558
  );
559
 
@@ -572,19 +615,27 @@ export const useSendMessage = (
572
  [conversationId, setConversation, sendMessage],
573
  );
574
 
575
- const handlePressEnter = () => {
576
- if (!loading) {
 
 
 
 
 
 
 
 
577
  setValue('');
578
- addNewestConversation(value);
579
  handleSendMessage(value.trim());
580
  }
581
- };
 
582
 
583
  return {
584
  handlePressEnter,
585
  handleInputChange,
586
  value,
587
- loading,
588
  };
589
  };
590
 
 
1
  import { MessageType } from '@/constants/chat';
2
  import { fileIconMap } from '@/constants/common';
3
  import {
 
4
  useCreateToken,
5
  useFetchConversation,
6
  useFetchConversationList,
 
23
  useShowDeleteConfirm,
24
  useTranslate,
25
  } from '@/hooks/commonHooks';
26
+ import { useSendMessageWithSse } from '@/hooks/logicHooks';
27
  import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
28
+ import {
29
+ IAnswer,
30
+ IConversation,
31
+ IDialog,
32
+ IStats,
33
+ } from '@/interfaces/database/chat';
34
  import { IChunk } from '@/interfaces/database/knowledge';
35
  import { getFileExtension } from '@/utils';
36
  import { message } from 'antd';
 
385
  const dialog = useSelectCurrentDialog();
386
  const { conversationId, dialogId } = useGetChatSearchParams();
387
 
388
+ const addNewestConversation = useCallback(
389
+ (message: string, answer: string = '') => {
390
+ setCurrentConversation((pre) => {
391
+ return {
392
+ ...pre,
393
+ message: [
394
+ ...pre.message,
395
+ {
396
+ role: MessageType.User,
397
+ content: message,
398
+ id: uuid(),
399
+ } as IMessage,
400
+ {
401
+ role: MessageType.Assistant,
402
+ content: answer,
403
+ id: uuid(),
404
+ reference: [],
405
+ } as IMessage,
406
+ ],
407
+ };
408
+ });
409
+ },
410
+ [],
411
+ );
412
+
413
+ const addNewestAnswer = useCallback((answer: IAnswer) => {
414
  setCurrentConversation((pre) => {
415
+ const latestMessage = pre.message?.at(-1);
416
+
417
+ if (latestMessage) {
418
+ return {
419
+ ...pre,
420
+ message: [
421
+ ...pre.message.slice(0, -1),
422
+ {
423
+ ...latestMessage,
424
+ content: answer.answer,
425
+ reference: answer.reference,
426
+ } as IMessage,
427
+ ],
428
+ };
429
+ }
430
+ return pre;
 
431
  });
432
  }, []);
433
 
434
  const removeLatestMessage = useCallback(() => {
435
+ console.info('removeLatestMessage');
436
  setCurrentConversation((pre) => {
437
+ const nextMessages = pre.message?.slice(0, -2) ?? [];
438
  return {
439
  ...pre,
440
  message: nextMessages,
 
471
  }
472
  }, [conversation, conversationId]);
473
 
474
+ return {
475
+ currentConversation,
476
+ addNewestConversation,
477
+ removeLatestMessage,
478
+ addNewestAnswer,
479
+ };
480
  };
481
 
482
  export const useScrollToBottom = (currentConversation: IClientConversation) => {
 
499
  export const useFetchConversationOnMount = () => {
500
  const { conversationId } = useGetChatSearchParams();
501
  const fetchConversation = useFetchConversation();
502
+ const {
503
+ currentConversation,
504
+ addNewestConversation,
505
+ removeLatestMessage,
506
+ addNewestAnswer,
507
+ } = useSelectCurrentConversation();
508
  const ref = useScrollToBottom(currentConversation);
509
 
510
  const fetchConversationOnMount = useCallback(() => {
 
522
  addNewestConversation,
523
  ref,
524
  removeLatestMessage,
525
+ addNewestAnswer,
526
  };
527
  };
528
 
 
544
 
545
  export const useSendMessage = (
546
  conversation: IClientConversation,
547
+ addNewestConversation: (message: string, answer?: string) => void,
548
  removeLatestMessage: () => void,
549
+ addNewestAnswer: (answer: IAnswer) => void,
550
  ) => {
 
 
 
551
  const { setConversation } = useSetConversation();
552
  const { conversationId } = useGetChatSearchParams();
553
  const { handleInputChange, value, setValue } = useHandleMessageInputChange();
554
 
555
  const fetchConversation = useFetchConversation();
 
556
 
557
  const { handleClickConversation } = useClickConversationCard();
558
+ const { send, answer, done } = useSendMessageWithSse();
559
 
560
  const sendMessage = useCallback(
561
  async (message: string, id?: string) => {
562
+ const res: Response = await send({
563
  conversation_id: id ?? conversationId,
564
  messages: [
565
  ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
 
570
  ],
571
  });
572
 
573
+ if (res.status === 200) {
574
  if (id) {
575
+ console.info('111');
576
  // new conversation
577
  handleClickConversation(id);
578
  } else {
579
+ console.info('222');
580
+ // fetchConversation(conversationId);
581
  }
582
  } else {
583
+ console.info('333');
584
+
585
  // cancel loading
586
  setValue(message);
587
+ console.info('removeLatestMessage111');
588
  removeLatestMessage();
589
  }
590
+ console.info('false');
591
  },
592
  [
593
  conversation?.message,
594
  conversationId,
595
+ // fetchConversation,
596
  handleClickConversation,
597
  removeLatestMessage,
598
  setValue,
599
+ send,
600
  ],
601
  );
602
 
 
615
  [conversationId, setConversation, sendMessage],
616
  );
617
 
618
+ useEffect(() => {
619
+ if (answer.answer) {
620
+ addNewestAnswer(answer);
621
+ console.info('true?');
622
+ console.info('send msg:', answer.answer);
623
+ }
624
+ }, [answer, addNewestAnswer]);
625
+
626
+ const handlePressEnter = useCallback(() => {
627
+ if (done) {
628
  setValue('');
 
629
  handleSendMessage(value.trim());
630
  }
631
+ addNewestConversation(value);
632
+ }, [addNewestConversation, handleSendMessage, done, setValue, value]);
633
 
634
  return {
635
  handlePressEnter,
636
  handleInputChange,
637
  value,
638
+ loading: !done,
639
  };
640
  };
641
 
web/src/pages/chat/interface.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { IConversation, Message } from '@/interfaces/database/chat';
2
  import { FormInstance } from 'antd';
3
 
4
  export interface ISegmentedContentProps {
@@ -24,6 +24,7 @@ export type IPromptConfigParameters = Omit<VariableTableDataType, 'variable'>;
24
 
25
  export interface IMessage extends Message {
26
  id: string;
 
27
  }
28
 
29
  export interface IClientConversation extends IConversation {
 
1
+ import { IConversation, IReference, Message } from '@/interfaces/database/chat';
2
  import { FormInstance } from 'antd';
3
 
4
  export interface ISegmentedContentProps {
 
24
 
25
  export interface IMessage extends Message {
26
  id: string;
27
+ reference?: IReference; // the latest news has reference
28
  }
29
 
30
  export interface IClientConversation extends IConversation {
web/src/pages/chat/markdown-content/index.less CHANGED
@@ -23,3 +23,23 @@
23
  .referenceIcon {
24
  padding: 0 6px;
25
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  .referenceIcon {
24
  padding: 0 6px;
25
  }
26
+
27
+ .cursor {
28
+ display: inline-block;
29
+ width: 1px;
30
+ height: 16px;
31
+ background-color: black;
32
+ animation: blink 0.6s infinite;
33
+ vertical-align: text-top;
34
+ @keyframes blink {
35
+ 0% {
36
+ opacity: 1;
37
+ }
38
+ 50% {
39
+ opacity: 0;
40
+ }
41
+ 100% {
42
+ opacity: 1;
43
+ }
44
+ }
45
+ }
web/src/pages/chat/markdown-content/index.tsx CHANGED
@@ -16,6 +16,7 @@ import { visitParents } from 'unist-util-visit-parents';
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.
@@ -61,7 +62,7 @@ const MarkdownContent = ({
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;
@@ -129,7 +130,7 @@ const MarkdownContent = ({
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)}>
@@ -137,6 +138,12 @@ const MarkdownContent = ({
137
  </Popover>
138
  );
139
  });
 
 
 
 
 
 
140
  },
141
  [getPopoverContent],
142
  );
 
16
  import styles from './index.less';
17
 
18
  const reg = /(#{2}\d+\${2})/g;
19
+ const curReg = /(~{2}\d+\${2})/g;
20
 
21
  const getChunkIndex = (match: string) => Number(match.slice(2, -2));
22
  // TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
 
62
  (chunkIndex: number) => {
63
  const chunks = reference?.chunks ?? [];
64
  const chunkItem = chunks[chunkIndex];
65
+ const document = reference?.doc_aggs?.find(
66
  (x) => x?.doc_id === chunkItem?.doc_id,
67
  );
68
  const documentId = document?.doc_id;
 
130
 
131
  const renderReference = useCallback(
132
  (text: string) => {
133
+ let replacedText = reactStringReplace(text, reg, (match, i) => {
134
  const chunkIndex = getChunkIndex(match);
135
  return (
136
  <Popover content={getPopoverContent(chunkIndex)}>
 
138
  </Popover>
139
  );
140
  });
141
+
142
+ replacedText = reactStringReplace(replacedText, curReg, (match, i) => (
143
+ <span className={styles.cursor} key={i}></span>
144
+ ));
145
+
146
+ return replacedText;
147
  },
148
  [getPopoverContent],
149
  );
web/src/pages/chat/share/index.tsx CHANGED
@@ -1,51 +1,11 @@
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
  };
 
 
 
 
 
 
 
1
  import ChatContainer from './large';
2
 
3
  import styles from './index.less';
4
 
5
  const SharedChat = () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  return (
7
  <div className={styles.chatWrapper}>
8
+ <ChatContainer></ChatContainer>
 
 
 
 
 
 
 
 
9
  </div>
10
  );
11
  };
web/src/pages/chat/share/large.tsx CHANGED
@@ -1,18 +1,50 @@
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 HightLightMarkdown from '@/components/highlight-markdown';
10
- import React, { ChangeEventHandler, forwardRef } from 'react';
11
- import { IClientConversation } from '../interface';
 
 
 
 
 
 
 
 
 
 
12
  import styles from './index.less';
13
 
14
- const MessageItem = ({ item }: { item: Message }) => {
 
 
 
 
 
 
 
 
15
  const isAssistant = item.role === MessageType.Assistant;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  return (
18
  <div
@@ -45,12 +77,43 @@ const MessageItem = ({ item }: { item: Message }) => {
45
  <Flex vertical gap={8} flex={1}>
46
  <b>{isAssistant ? '' : 'You'}</b>
47
  <div className={styles.messageText}>
48
- {item.content !== '' ? (
49
- <HightLightMarkdown>{item.content}</HightLightMarkdown>
50
- ) : (
51
- <Skeleton active className={styles.messageEmpty} />
52
- )}
53
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  </Flex>
55
  </div>
56
  </section>
@@ -58,28 +121,31 @@ const MessageItem = ({ item }: { item: Message }) => {
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
  <>
@@ -87,9 +153,18 @@ const ChatContainer = (
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>
 
1
  import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
2
  import { MessageType } from '@/constants/chat';
3
  import { useTranslate } from '@/hooks/commonHooks';
4
+ import { IReference, Message } from '@/interfaces/database/chat';
5
+ import { Avatar, Button, Flex, Input, List, Spin } from 'antd';
6
  import classNames from 'classnames';
 
7
 
8
+ import NewDocumentLink from '@/components/new-document-link';
9
+ import SvgIcon from '@/components/svg-icon';
10
+ import { useGetDocumentUrl } from '@/hooks/documentHooks';
11
+ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
12
+ import { getExtension, isPdf } from '@/utils/documentUtils';
13
+ import { forwardRef, useMemo } from 'react';
14
+ import MarkdownContent from '../markdown-content';
15
+ import {
16
+ useCreateSharedConversationOnMount,
17
+ useSelectCurrentSharedConversation,
18
+ useSendSharedMessage,
19
+ } from '../shared-hooks';
20
+ import { buildMessageItemReference } from '../utils';
21
  import styles from './index.less';
22
 
23
+ const MessageItem = ({
24
+ item,
25
+ reference,
26
+ loading = false,
27
+ }: {
28
+ item: Message;
29
+ reference: IReference;
30
+ loading?: boolean;
31
+ }) => {
32
  const isAssistant = item.role === MessageType.Assistant;
33
+ const { t } = useTranslate('chat');
34
+ const fileThumbnails = useSelectFileThumbnails();
35
+ const getDocumentUrl = useGetDocumentUrl();
36
+
37
+ const referenceDocumentList = useMemo(() => {
38
+ return reference?.doc_aggs ?? [];
39
+ }, [reference?.doc_aggs]);
40
+
41
+ const content = useMemo(() => {
42
+ let text = item.content;
43
+ if (text === '') {
44
+ text = t('searching');
45
+ }
46
+ return loading ? text?.concat('~~2$$') : text;
47
+ }, [item.content, loading, t]);
48
 
49
  return (
50
  <div
 
77
  <Flex vertical gap={8} flex={1}>
78
  <b>{isAssistant ? '' : 'You'}</b>
79
  <div className={styles.messageText}>
80
+ <MarkdownContent
81
+ reference={reference}
82
+ clickDocumentButton={() => {}}
83
+ content={content}
84
+ ></MarkdownContent>
85
  </div>
86
+ {isAssistant && referenceDocumentList.length > 0 && (
87
+ <List
88
+ bordered
89
+ dataSource={referenceDocumentList}
90
+ renderItem={(item) => {
91
+ const fileThumbnail = fileThumbnails[item.doc_id];
92
+ const fileExtension = getExtension(item.doc_name);
93
+ return (
94
+ <List.Item>
95
+ <Flex gap={'small'} align="center">
96
+ {fileThumbnail ? (
97
+ <img src={fileThumbnail}></img>
98
+ ) : (
99
+ <SvgIcon
100
+ name={`file-icon/${fileExtension}`}
101
+ width={24}
102
+ ></SvgIcon>
103
+ )}
104
+
105
+ <NewDocumentLink
106
+ link={getDocumentUrl(item.doc_id)}
107
+ preventDefault={!isPdf(item.doc_name)}
108
+ >
109
+ {item.doc_name}
110
+ </NewDocumentLink>
111
+ </Flex>
112
+ </List.Item>
113
+ );
114
+ }}
115
+ />
116
+ )}
117
  </Flex>
118
  </div>
119
  </section>
 
121
  );
122
  };
123
 
124
+ const ChatContainer = () => {
125
+ const { t } = useTranslate('chat');
126
+ const { conversationId } = useCreateSharedConversationOnMount();
127
+ const {
128
+ currentConversation: conversation,
129
+ addNewestConversation,
130
+ removeLatestMessage,
131
+ ref,
132
+ loading,
133
+ setCurrentConversation,
134
+ addNewestAnswer,
135
+ } = useSelectCurrentSharedConversation(conversationId);
136
 
137
+ const {
 
138
  handlePressEnter,
139
  handleInputChange,
140
  value,
141
  loading: sendLoading,
142
+ } = useSendSharedMessage(
143
  conversation,
144
+ addNewestConversation,
145
+ removeLatestMessage,
146
+ setCurrentConversation,
147
+ addNewestAnswer,
148
+ );
149
 
150
  return (
151
  <>
 
153
  <Flex flex={1} vertical className={styles.messageContainer}>
154
  <div>
155
  <Spin spinning={loading}>
156
+ {conversation?.message?.map((message, i) => {
157
  return (
158
+ <MessageItem
159
+ key={message.id}
160
+ item={message}
161
+ reference={buildMessageItemReference(conversation, message)}
162
+ loading={
163
+ message.role === MessageType.Assistant &&
164
+ sendLoading &&
165
+ conversation?.message.length - 1 === i
166
+ }
167
+ ></MessageItem>
168
  );
169
  })}
170
  </Spin>
web/src/pages/chat/shared-hooks.ts CHANGED
@@ -1,10 +1,12 @@
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,
@@ -76,6 +78,27 @@ export const useSelectCurrentSharedConversation = (conversationId: string) => {
76
  });
77
  }, []);
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  const removeLatestMessage = useCallback(() => {
80
  setCurrentConversation((pre) => {
81
  const nextMessages = pre.message.slice(0, -2);
@@ -106,6 +129,7 @@ export const useSelectCurrentSharedConversation = (conversationId: string) => {
106
  loading,
107
  ref,
108
  setCurrentConversation,
 
109
  };
110
  };
111
 
@@ -114,20 +138,19 @@ export const useSendSharedMessage = (
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
  quote: false,
133
  messages: [
@@ -139,11 +162,11 @@ export const useSendSharedMessage = (
139
  ],
140
  });
141
 
142
- if (retcode === 0) {
143
- const data = await fetchConversation(conversationId);
144
- if (data.retcode === 0) {
145
- setCurrentConversation(data.data);
146
- }
147
  } else {
148
  // cancel loading
149
  setValue(message);
@@ -153,11 +176,11 @@ export const useSendSharedMessage = (
153
  [
154
  conversationId,
155
  conversation?.message,
156
- fetchConversation,
157
  removeLatestMessage,
158
  setValue,
159
- completeConversation,
160
- setCurrentConversation,
161
  ],
162
  );
163
 
@@ -176,18 +199,24 @@ export const useSendSharedMessage = (
176
  [conversationId, setConversation, sendMessage],
177
  );
178
 
179
- const handlePressEnter = () => {
180
- if (!loading) {
 
 
 
 
 
 
181
  setValue('');
182
  addNewestConversation(value);
183
  handleSendMessage(value.trim());
184
  }
185
- };
186
 
187
  return {
188
  handlePressEnter,
189
  handleInputChange,
190
  value,
191
- loading,
192
  };
193
  };
 
1
  import { MessageType } from '@/constants/chat';
2
  import {
 
3
  useCreateSharedConversation,
4
  useFetchSharedConversation,
5
  } from '@/hooks/chatHooks';
6
+ import { useSendMessageWithSse } from '@/hooks/logicHooks';
7
  import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
8
+ import { IAnswer } from '@/interfaces/database/chat';
9
+ import api from '@/utils/api';
10
  import omit from 'lodash/omit';
11
  import {
12
  Dispatch,
 
78
  });
79
  }, []);
80
 
81
+ const addNewestAnswer = useCallback((answer: IAnswer) => {
82
+ setCurrentConversation((pre) => {
83
+ const latestMessage = pre.message?.at(-1);
84
+
85
+ if (latestMessage) {
86
+ return {
87
+ ...pre,
88
+ message: [
89
+ ...pre.message.slice(0, -1),
90
+ {
91
+ ...latestMessage,
92
+ content: answer.answer,
93
+ reference: answer.reference,
94
+ } as IMessage,
95
+ ],
96
+ };
97
+ }
98
+ return pre;
99
+ });
100
+ }, []);
101
+
102
  const removeLatestMessage = useCallback(() => {
103
  setCurrentConversation((pre) => {
104
  const nextMessages = pre.message.slice(0, -2);
 
129
  loading,
130
  ref,
131
  setCurrentConversation,
132
+ addNewestAnswer,
133
  };
134
  };
135
 
 
138
  addNewestConversation: (message: string) => void,
139
  removeLatestMessage: () => void,
140
  setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>,
141
+ addNewestAnswer: (answer: IAnswer) => void,
142
  ) => {
143
  const conversationId = conversation.id;
 
 
 
144
  const setConversation = useCreateSharedConversation();
145
  const { handleInputChange, value, setValue } = useHandleMessageInputChange();
146
 
147
+ const { send, answer, done } = useSendMessageWithSse(
148
+ api.completeExternalConversation,
149
+ );
150
 
151
  const sendMessage = useCallback(
152
  async (message: string, id?: string) => {
153
+ const res: Response = await send({
154
  conversation_id: id ?? conversationId,
155
  quote: false,
156
  messages: [
 
162
  ],
163
  });
164
 
165
+ if (res?.status === 200) {
166
+ // const data = await fetchConversation(conversationId);
167
+ // if (data.retcode === 0) {
168
+ // setCurrentConversation(data.data);
169
+ // }
170
  } else {
171
  // cancel loading
172
  setValue(message);
 
176
  [
177
  conversationId,
178
  conversation?.message,
179
+ // fetchConversation,
180
  removeLatestMessage,
181
  setValue,
182
+ send,
183
+ // setCurrentConversation,
184
  ],
185
  );
186
 
 
199
  [conversationId, setConversation, sendMessage],
200
  );
201
 
202
+ useEffect(() => {
203
+ if (answer.answer) {
204
+ addNewestAnswer(answer);
205
+ }
206
+ }, [answer, addNewestAnswer]);
207
+
208
+ const handlePressEnter = useCallback(() => {
209
+ if (done) {
210
  setValue('');
211
  addNewestConversation(value);
212
  handleSendMessage(value.trim());
213
  }
214
+ }, [addNewestConversation, done, handleSendMessage, setValue, value]);
215
 
216
  return {
217
  handlePressEnter,
218
  handleInputChange,
219
  value,
220
+ loading: !done,
221
  };
222
  };
web/src/pages/chat/utils.ts CHANGED
@@ -1,5 +1,7 @@
 
1
  import { IConversation, IReference } from '@/interfaces/database/chat';
2
  import { EmptyConversationId, variableEnabledFieldMap } from './constants';
 
3
 
4
  export const excludeUnEnabledVariables = (values: any) => {
5
  const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> =
@@ -20,7 +22,7 @@ export const getDocumentIdsFromConversionReference = (data: IConversation) => {
20
  const documentIds = data.reference.reduce(
21
  (pre: Array<string>, cur: IReference) => {
22
  cur.doc_aggs
23
- .map((x) => x.doc_id)
24
  .forEach((x) => {
25
  if (pre.every((y) => y !== x)) {
26
  pre.push(x);
@@ -32,3 +34,20 @@ export const getDocumentIdsFromConversionReference = (data: IConversation) => {
32
  );
33
  return documentIds.join(',');
34
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MessageType } from '@/constants/chat';
2
  import { IConversation, IReference } from '@/interfaces/database/chat';
3
  import { EmptyConversationId, variableEnabledFieldMap } from './constants';
4
+ import { IClientConversation, IMessage } from './interface';
5
 
6
  export const excludeUnEnabledVariables = (values: any) => {
7
  const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> =
 
22
  const documentIds = data.reference.reduce(
23
  (pre: Array<string>, cur: IReference) => {
24
  cur.doc_aggs
25
+ ?.map((x) => x.doc_id)
26
  .forEach((x) => {
27
  if (pre.every((y) => y !== x)) {
28
  pre.push(x);
 
34
  );
35
  return documentIds.join(',');
36
  };
37
+
38
+ export const buildMessageItemReference = (
39
+ conversation: IClientConversation,
40
+ message: IMessage,
41
+ ) => {
42
+ const assistantMessages = conversation.message
43
+ ?.filter((x) => x.role === MessageType.Assistant)
44
+ .slice(1);
45
+ const referenceIndex = assistantMessages.findIndex(
46
+ (x) => x.id === message.id,
47
+ );
48
+ const reference = message?.reference
49
+ ? message?.reference
50
+ : conversation.reference[referenceIndex];
51
+
52
+ return reference;
53
+ };
web/src/utils/authorizationUtil.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { Authorization, Token, UserInfo } from '@/constants/authorization';
2
-
3
  const KeySet = [Authorization, Token, UserInfo];
4
 
5
  const storage = {
@@ -21,7 +21,7 @@ const storage = {
21
  setToken: (value: string) => {
22
  localStorage.setItem(Token, value);
23
  },
24
- setUserInfo: (value: string | Object) => {
25
  let valueStr = typeof value !== 'string' ? JSON.stringify(value) : value;
26
  localStorage.setItem(UserInfo, valueStr);
27
  },
@@ -46,4 +46,13 @@ const storage = {
46
  },
47
  };
48
 
 
 
 
 
 
 
 
 
 
49
  export default storage;
 
1
  import { Authorization, Token, UserInfo } from '@/constants/authorization';
2
+ import { getSearchValue } from './commonUtil';
3
  const KeySet = [Authorization, Token, UserInfo];
4
 
5
  const storage = {
 
21
  setToken: (value: string) => {
22
  localStorage.setItem(Token, value);
23
  },
24
+ setUserInfo: (value: string | Record<string, unknown>) => {
25
  let valueStr = typeof value !== 'string' ? JSON.stringify(value) : value;
26
  localStorage.setItem(UserInfo, valueStr);
27
  },
 
46
  },
47
  };
48
 
49
+ export const getAuthorization = () => {
50
+ const sharedId = getSearchValue('shared_id');
51
+ const authorization = sharedId
52
+ ? 'Bearer ' + sharedId
53
+ : storage.getAuthorization() || '';
54
+
55
+ return authorization;
56
+ };
57
+
58
  export default storage;
web/src/utils/request.ts CHANGED
@@ -1,12 +1,12 @@
1
  import { Authorization } from '@/constants/authorization';
2
  import i18n from '@/locales/config';
3
- 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, getSearchValue } from './commonUtil';
8
 
9
- const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message
10
 
11
  const RetcodeMessage = {
12
  200: i18n.t('message.200'),
@@ -41,9 +41,7 @@ type ResultCode =
41
  | 502
42
  | 503
43
  | 504;
44
- /**
45
- * 异常处理程序
46
- */
47
  interface ResponseType {
48
  retcode: number;
49
  data: any;
@@ -55,7 +53,6 @@ const errorHandler = (error: {
55
  message: string;
56
  }): Response => {
57
  const { response } = error;
58
- // 手动中断请求 abort
59
  if (error.message === ABORT_REQUEST_ERR_MESSAGE) {
60
  console.log('user abort request');
61
  } else {
@@ -77,20 +74,13 @@ const errorHandler = (error: {
77
  return response;
78
  };
79
 
80
- /**
81
- * 配置request请求时的默认参数
82
- */
83
  const request: RequestMethod = extend({
84
- errorHandler, // 默认错误处理
85
  timeout: 300000,
86
  getResponse: true,
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
 
@@ -101,7 +91,9 @@ request.interceptors.request.use((url: string, options: any) => {
101
  data,
102
  params,
103
  headers: {
104
- ...(options.skipToken ? undefined : { [Authorization]: authorization }),
 
 
105
  ...options.headers,
106
  },
107
  interceptors: true,
@@ -109,16 +101,11 @@ request.interceptors.request.use((url: string, options: any) => {
109
  };
110
  });
111
 
112
- /*
113
- * 请求response拦截器
114
- * */
115
-
116
  request.interceptors.response.use(async (response: any, options) => {
117
  if (options.responseType === 'blob') {
118
  return response;
119
  }
120
  const data: ResponseType = await response.clone().json();
121
- // response 拦截
122
 
123
  if (data.retcode === 401 || data.retcode === 401) {
124
  notification.error({
 
1
  import { Authorization } from '@/constants/authorization';
2
  import i18n from '@/locales/config';
3
+ import authorizationUtil, { getAuthorization } 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.';
10
 
11
  const RetcodeMessage = {
12
  200: i18n.t('message.200'),
 
41
  | 502
42
  | 503
43
  | 504;
44
+
 
 
45
  interface ResponseType {
46
  retcode: number;
47
  data: any;
 
53
  message: string;
54
  }): Response => {
55
  const { response } = error;
 
56
  if (error.message === ABORT_REQUEST_ERR_MESSAGE) {
57
  console.log('user abort request');
58
  } else {
 
74
  return response;
75
  };
76
 
 
 
 
77
  const request: RequestMethod = extend({
78
+ errorHandler,
79
  timeout: 300000,
80
  getResponse: true,
81
  });
82
 
83
  request.interceptors.request.use((url: string, options: any) => {
 
 
 
 
84
  const data = convertTheKeysOfTheObjectToSnake(options.data);
85
  const params = convertTheKeysOfTheObjectToSnake(options.params);
86
 
 
91
  data,
92
  params,
93
  headers: {
94
+ ...(options.skipToken
95
+ ? undefined
96
+ : { [Authorization]: getAuthorization() }),
97
  ...options.headers,
98
  },
99
  interceptors: true,
 
101
  };
102
  });
103
 
 
 
 
 
104
  request.interceptors.response.use(async (response: any, options) => {
105
  if (options.responseType === 'blob') {
106
  return response;
107
  }
108
  const data: ResponseType = await response.clone().json();
 
109
 
110
  if (data.retcode === 401 || data.retcode === 401) {
111
  notification.error({