mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-06-04 11:24:00 +08:00
feat: Support for conversational streaming (#809)
### What problem does this PR solve? feat: Support for conversational streaming #709 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
parent
95f809187e
commit
c6c9dbde64
9
web/package-lock.json
generated
9
web/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"axios": "^1.6.3",
|
"axios": "^1.6.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"eventsource-parser": "^1.1.2",
|
||||||
"i18next": "^23.7.16",
|
"i18next": "^23.7.16",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.5",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
@ -10206,6 +10207,14 @@
|
|||||||
"node": ">=0.8.x"
|
"node": ">=0.8.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventsource-parser": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/evp_bytestokey": {
|
"node_modules/evp_bytestokey": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"author": "zhaofengchao <13723060510@163.com>",
|
"author": "zhaofengchao <13723060510@163.com>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "umi build",
|
"build": "umi build",
|
||||||
"dev": "cross-env PORT=9200 UMI_DEV_SERVER_COMPRESS=none umi dev",
|
"dev": "cross-env UMI_DEV_SERVER_COMPRESS=none umi dev",
|
||||||
"postinstall": "umi setup",
|
"postinstall": "umi setup",
|
||||||
"lint": "umi lint --eslint-only",
|
"lint": "umi lint --eslint-only",
|
||||||
"setup": "umi setup",
|
"setup": "umi setup",
|
||||||
@ -19,6 +19,7 @@
|
|||||||
"axios": "^1.6.3",
|
"axios": "^1.6.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"eventsource-parser": "^1.1.2",
|
||||||
"i18next": "^23.7.16",
|
"i18next": "^23.7.16",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.5",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
|
@ -18,7 +18,7 @@ const NewDocumentLink = ({
|
|||||||
onClick={!preventDefault ? undefined : (e) => e.preventDefault()}
|
onClick={!preventDefault ? undefined : (e) => e.preventDefault()}
|
||||||
href={link}
|
href={link}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
style={{ color }}
|
style={{ color, wordBreak: 'break-all' }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
|
@ -154,6 +154,9 @@ export const useRemoveConversation = () => {
|
|||||||
return removeConversation;
|
return removeConversation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
@deprecated
|
||||||
|
*/
|
||||||
export const useCompleteConversation = () => {
|
export const useCompleteConversation = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@ -283,20 +286,4 @@ export const useFetchSharedConversation = () => {
|
|||||||
return fetchSharedConversation;
|
return fetchSharedConversation;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCompleteSharedConversation = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const completeSharedConversation = useCallback(
|
|
||||||
(payload: any) => {
|
|
||||||
return dispatch<any>({
|
|
||||||
type: 'chatModel/completeExternalConversation',
|
|
||||||
payload: payload,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
return completeSharedConversation;
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { Authorization } from '@/constants/authorization';
|
import { Authorization } from '@/constants/authorization';
|
||||||
import { LanguageTranslationMap } from '@/constants/common';
|
import { LanguageTranslationMap } from '@/constants/common';
|
||||||
import { Pagination } from '@/interfaces/common';
|
import { Pagination } from '@/interfaces/common';
|
||||||
|
import { IAnswer } from '@/interfaces/database/chat';
|
||||||
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||||
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
|
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import authorizationUtil from '@/utils/authorizationUtil';
|
import { getAuthorization } from '@/utils/authorizationUtil';
|
||||||
import { getSearchValue } from '@/utils/commonUtil';
|
|
||||||
import { PaginationProps } from 'antd';
|
import { PaginationProps } from 'antd';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'umi';
|
import { useDispatch } from 'umi';
|
||||||
@ -138,62 +139,60 @@ export const useFetchAppConf = () => {
|
|||||||
return appConf;
|
return appConf;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useConnectWithSse = (url: string) => {
|
export const useSendMessageWithSse = (
|
||||||
const [content, setContent] = useState<string>('');
|
url: string = api.completeConversation,
|
||||||
|
) => {
|
||||||
|
const [answer, setAnswer] = useState<IAnswer>({} as IAnswer);
|
||||||
|
const [done, setDone] = useState(true);
|
||||||
|
|
||||||
const connect = useCallback(() => {
|
|
||||||
const source = new EventSource(
|
|
||||||
url || '/sse/createSseEmitter?clientId=123456',
|
|
||||||
);
|
|
||||||
|
|
||||||
source.onopen = function () {
|
|
||||||
console.log('Connection to the server was opened.');
|
|
||||||
};
|
|
||||||
|
|
||||||
source.onmessage = function (event: any) {
|
|
||||||
setContent(event.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
source.onerror = function (error) {
|
|
||||||
console.error('Error occurred:', error);
|
|
||||||
};
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
return { connect, content };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useConnectWithSseNext = () => {
|
|
||||||
const [content, setContent] = useState<string>('');
|
|
||||||
const sharedId = getSearchValue('shared_id');
|
|
||||||
const authorization = sharedId
|
|
||||||
? 'Bearer ' + sharedId
|
|
||||||
: authorizationUtil.getAuthorization();
|
|
||||||
const send = useCallback(
|
const send = useCallback(
|
||||||
async (body: any) => {
|
async (body: any) => {
|
||||||
const response = await fetch(api.completeConversation, {
|
try {
|
||||||
|
setDone(false);
|
||||||
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
[Authorization]: authorization,
|
[Authorization]: getAuthorization(),
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
const reader = response?.body
|
const reader = response?.body
|
||||||
?.pipeThrough(new TextDecoderStream())
|
?.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeThrough(new EventSourceParserStream())
|
||||||
.getReader();
|
.getReader();
|
||||||
|
|
||||||
// const reader = response.body.getReader();
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = await reader?.read();
|
const x = await reader?.read();
|
||||||
console.log('Received', value);
|
if (x) {
|
||||||
setContent(value);
|
const { done, value } = x;
|
||||||
if (done) break;
|
try {
|
||||||
|
const val = JSON.parse(value?.data || '');
|
||||||
|
const d = val?.data;
|
||||||
|
if (typeof d !== 'boolean') {
|
||||||
|
console.info('data:', d);
|
||||||
|
setAnswer(d);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
console.info('done');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info('done?');
|
||||||
|
setDone(true);
|
||||||
return response;
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
setDone(true);
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[authorization],
|
[url],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { send, content };
|
return { send, answer, done };
|
||||||
};
|
};
|
||||||
|
@ -72,6 +72,11 @@ export interface IReference {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IAnswer {
|
||||||
|
answer: string;
|
||||||
|
reference: IReference;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Docagg {
|
export interface Docagg {
|
||||||
count: number;
|
count: number;
|
||||||
doc_id: string;
|
doc_id: string;
|
||||||
|
@ -25,6 +25,7 @@ export default {
|
|||||||
comingSoon: 'Coming Soon',
|
comingSoon: 'Coming Soon',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
|
preview: 'Preview',
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
login: 'Sign in',
|
login: 'Sign in',
|
||||||
@ -381,6 +382,7 @@ export default {
|
|||||||
partialTitle: 'Partial Embed',
|
partialTitle: 'Partial Embed',
|
||||||
extensionTitle: 'Chrome Extension',
|
extensionTitle: 'Chrome Extension',
|
||||||
tokenError: 'Please create API Token first!',
|
tokenError: 'Please create API Token first!',
|
||||||
|
searching: 'searching...',
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
profile: 'Profile',
|
profile: 'Profile',
|
||||||
|
@ -25,6 +25,7 @@ export default {
|
|||||||
comingSoon: '即將推出',
|
comingSoon: '即將推出',
|
||||||
download: '下載',
|
download: '下載',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
|
preview: '預覽',
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
login: '登入',
|
login: '登入',
|
||||||
@ -352,6 +353,7 @@ export default {
|
|||||||
partialTitle: '部分嵌入',
|
partialTitle: '部分嵌入',
|
||||||
extensionTitle: 'Chrome 插件',
|
extensionTitle: 'Chrome 插件',
|
||||||
tokenError: '請先創建 Api Token!',
|
tokenError: '請先創建 Api Token!',
|
||||||
|
searching: '搜索中',
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
profile: '概述',
|
profile: '概述',
|
||||||
|
@ -25,6 +25,7 @@ export default {
|
|||||||
comingSoon: '即将推出',
|
comingSoon: '即将推出',
|
||||||
download: '下载',
|
download: '下载',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
|
preview: '预览',
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
@ -369,6 +370,7 @@ export default {
|
|||||||
partialTitle: '部分嵌入',
|
partialTitle: '部分嵌入',
|
||||||
extensionTitle: 'Chrome 插件',
|
extensionTitle: 'Chrome 插件',
|
||||||
tokenError: '请先创建 Api Token!',
|
tokenError: '请先创建 Api Token!',
|
||||||
|
searching: '搜索中',
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
profile: '概要',
|
profile: '概要',
|
||||||
|
@ -6,16 +6,7 @@ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
|
|||||||
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
||||||
import { IReference, Message } from '@/interfaces/database/chat';
|
import { IReference, Message } from '@/interfaces/database/chat';
|
||||||
import { IChunk } from '@/interfaces/database/knowledge';
|
import { IChunk } from '@/interfaces/database/knowledge';
|
||||||
import {
|
import { Avatar, Button, Drawer, Flex, Input, List, Spin } from 'antd';
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Drawer,
|
|
||||||
Flex,
|
|
||||||
Input,
|
|
||||||
List,
|
|
||||||
Skeleton,
|
|
||||||
Spin,
|
|
||||||
} from 'antd';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
@ -32,20 +23,24 @@ import SvgIcon from '@/components/svg-icon';
|
|||||||
import { useTranslate } from '@/hooks/commonHooks';
|
import { useTranslate } from '@/hooks/commonHooks';
|
||||||
import { useGetDocumentUrl } from '@/hooks/documentHooks';
|
import { useGetDocumentUrl } from '@/hooks/documentHooks';
|
||||||
import { getExtension, isPdf } from '@/utils/documentUtils';
|
import { getExtension, isPdf } from '@/utils/documentUtils';
|
||||||
|
import { buildMessageItemReference } from '../utils';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
|
||||||
const MessageItem = ({
|
const MessageItem = ({
|
||||||
item,
|
item,
|
||||||
reference,
|
reference,
|
||||||
|
loading = false,
|
||||||
clickDocumentButton,
|
clickDocumentButton,
|
||||||
}: {
|
}: {
|
||||||
item: Message;
|
item: Message;
|
||||||
reference: IReference;
|
reference: IReference;
|
||||||
|
loading?: boolean;
|
||||||
clickDocumentButton: (documentId: string, chunk: IChunk) => void;
|
clickDocumentButton: (documentId: string, chunk: IChunk) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const userInfo = useSelectUserInfo();
|
const userInfo = useSelectUserInfo();
|
||||||
const fileThumbnails = useSelectFileThumbnails();
|
const fileThumbnails = useSelectFileThumbnails();
|
||||||
const getDocumentUrl = useGetDocumentUrl();
|
const getDocumentUrl = useGetDocumentUrl();
|
||||||
|
const { t } = useTranslate('chat');
|
||||||
|
|
||||||
const isAssistant = item.role === MessageType.Assistant;
|
const isAssistant = item.role === MessageType.Assistant;
|
||||||
|
|
||||||
@ -53,6 +48,14 @@ const MessageItem = ({
|
|||||||
return reference?.doc_aggs ?? [];
|
return reference?.doc_aggs ?? [];
|
||||||
}, [reference?.doc_aggs]);
|
}, [reference?.doc_aggs]);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let text = item.content;
|
||||||
|
if (text === '') {
|
||||||
|
text = t('searching');
|
||||||
|
}
|
||||||
|
return loading ? text?.concat('~~2$$') : text;
|
||||||
|
}, [item.content, loading, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.messageItem, {
|
className={classNames(styles.messageItem, {
|
||||||
@ -85,15 +88,11 @@ const MessageItem = ({
|
|||||||
<Flex vertical gap={8} flex={1}>
|
<Flex vertical gap={8} flex={1}>
|
||||||
<b>{isAssistant ? '' : userInfo.nickname}</b>
|
<b>{isAssistant ? '' : userInfo.nickname}</b>
|
||||||
<div className={styles.messageText}>
|
<div className={styles.messageText}>
|
||||||
{item.content !== '' ? (
|
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
content={item.content}
|
content={content}
|
||||||
reference={reference}
|
reference={reference}
|
||||||
clickDocumentButton={clickDocumentButton}
|
clickDocumentButton={clickDocumentButton}
|
||||||
></MarkdownContent>
|
></MarkdownContent>
|
||||||
) : (
|
|
||||||
<Skeleton active className={styles.messageEmpty} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{isAssistant && referenceDocumentList.length > 0 && (
|
{isAssistant && referenceDocumentList.length > 0 && (
|
||||||
<List
|
<List
|
||||||
@ -139,13 +138,19 @@ const ChatContainer = () => {
|
|||||||
currentConversation: conversation,
|
currentConversation: conversation,
|
||||||
addNewestConversation,
|
addNewestConversation,
|
||||||
removeLatestMessage,
|
removeLatestMessage,
|
||||||
|
addNewestAnswer,
|
||||||
} = useFetchConversationOnMount();
|
} = useFetchConversationOnMount();
|
||||||
const {
|
const {
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
handlePressEnter,
|
handlePressEnter,
|
||||||
value,
|
value,
|
||||||
loading: sendLoading,
|
loading: sendLoading,
|
||||||
} = useSendMessage(conversation, addNewestConversation, removeLatestMessage);
|
} = useSendMessage(
|
||||||
|
conversation,
|
||||||
|
addNewestConversation,
|
||||||
|
removeLatestMessage,
|
||||||
|
addNewestAnswer,
|
||||||
|
);
|
||||||
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
||||||
useClickDrawer();
|
useClickDrawer();
|
||||||
const disabled = useGetSendButtonDisabled();
|
const disabled = useGetSendButtonDisabled();
|
||||||
@ -159,19 +164,17 @@ const ChatContainer = () => {
|
|||||||
<Flex flex={1} vertical className={styles.messageContainer}>
|
<Flex flex={1} vertical className={styles.messageContainer}>
|
||||||
<div>
|
<div>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{conversation?.message?.map((message) => {
|
{conversation?.message?.map((message, i) => {
|
||||||
const assistantMessages = conversation?.message
|
|
||||||
?.filter((x) => x.role === MessageType.Assistant)
|
|
||||||
.slice(1);
|
|
||||||
const referenceIndex = assistantMessages.findIndex(
|
|
||||||
(x) => x.id === message.id,
|
|
||||||
);
|
|
||||||
const reference = conversation.reference[referenceIndex];
|
|
||||||
return (
|
return (
|
||||||
<MessageItem
|
<MessageItem
|
||||||
|
loading={
|
||||||
|
message.role === MessageType.Assistant &&
|
||||||
|
sendLoading &&
|
||||||
|
conversation?.message.length - 1 === i
|
||||||
|
}
|
||||||
key={message.id}
|
key={message.id}
|
||||||
item={message}
|
item={message}
|
||||||
reference={reference}
|
reference={buildMessageItemReference(conversation, message)}
|
||||||
clickDocumentButton={clickDocumentButton}
|
clickDocumentButton={clickDocumentButton}
|
||||||
></MessageItem>
|
></MessageItem>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { MessageType } from '@/constants/chat';
|
import { MessageType } from '@/constants/chat';
|
||||||
import { fileIconMap } from '@/constants/common';
|
import { fileIconMap } from '@/constants/common';
|
||||||
import {
|
import {
|
||||||
useCompleteConversation,
|
|
||||||
useCreateToken,
|
useCreateToken,
|
||||||
useFetchConversation,
|
useFetchConversation,
|
||||||
useFetchConversationList,
|
useFetchConversationList,
|
||||||
@ -24,8 +23,14 @@ import {
|
|||||||
useShowDeleteConfirm,
|
useShowDeleteConfirm,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
} from '@/hooks/commonHooks';
|
} from '@/hooks/commonHooks';
|
||||||
|
import { useSendMessageWithSse } from '@/hooks/logicHooks';
|
||||||
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
||||||
import { IConversation, IDialog, IStats } from '@/interfaces/database/chat';
|
import {
|
||||||
|
IAnswer,
|
||||||
|
IConversation,
|
||||||
|
IDialog,
|
||||||
|
IStats,
|
||||||
|
} from '@/interfaces/database/chat';
|
||||||
import { IChunk } from '@/interfaces/database/knowledge';
|
import { IChunk } from '@/interfaces/database/knowledge';
|
||||||
import { getFileExtension } from '@/utils';
|
import { getFileExtension } from '@/utils';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
@ -380,7 +385,8 @@ export const useSelectCurrentConversation = () => {
|
|||||||
const dialog = useSelectCurrentDialog();
|
const dialog = useSelectCurrentDialog();
|
||||||
const { conversationId, dialogId } = useGetChatSearchParams();
|
const { conversationId, dialogId } = useGetChatSearchParams();
|
||||||
|
|
||||||
const addNewestConversation = useCallback((message: string) => {
|
const addNewestConversation = useCallback(
|
||||||
|
(message: string, answer: string = '') => {
|
||||||
setCurrentConversation((pre) => {
|
setCurrentConversation((pre) => {
|
||||||
return {
|
return {
|
||||||
...pre,
|
...pre,
|
||||||
@ -393,18 +399,42 @@ export const useSelectCurrentConversation = () => {
|
|||||||
} as IMessage,
|
} as IMessage,
|
||||||
{
|
{
|
||||||
role: MessageType.Assistant,
|
role: MessageType.Assistant,
|
||||||
content: '',
|
content: answer,
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
reference: [],
|
reference: [],
|
||||||
} as IMessage,
|
} as IMessage,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const addNewestAnswer = useCallback((answer: IAnswer) => {
|
||||||
|
setCurrentConversation((pre) => {
|
||||||
|
const latestMessage = pre.message?.at(-1);
|
||||||
|
|
||||||
|
if (latestMessage) {
|
||||||
|
return {
|
||||||
|
...pre,
|
||||||
|
message: [
|
||||||
|
...pre.message.slice(0, -1),
|
||||||
|
{
|
||||||
|
...latestMessage,
|
||||||
|
content: answer.answer,
|
||||||
|
reference: answer.reference,
|
||||||
|
} as IMessage,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return pre;
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const removeLatestMessage = useCallback(() => {
|
const removeLatestMessage = useCallback(() => {
|
||||||
|
console.info('removeLatestMessage');
|
||||||
setCurrentConversation((pre) => {
|
setCurrentConversation((pre) => {
|
||||||
const nextMessages = pre.message.slice(0, -2);
|
const nextMessages = pre.message?.slice(0, -2) ?? [];
|
||||||
return {
|
return {
|
||||||
...pre,
|
...pre,
|
||||||
message: nextMessages,
|
message: nextMessages,
|
||||||
@ -441,7 +471,12 @@ export const useSelectCurrentConversation = () => {
|
|||||||
}
|
}
|
||||||
}, [conversation, conversationId]);
|
}, [conversation, conversationId]);
|
||||||
|
|
||||||
return { currentConversation, addNewestConversation, removeLatestMessage };
|
return {
|
||||||
|
currentConversation,
|
||||||
|
addNewestConversation,
|
||||||
|
removeLatestMessage,
|
||||||
|
addNewestAnswer,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useScrollToBottom = (currentConversation: IClientConversation) => {
|
export const useScrollToBottom = (currentConversation: IClientConversation) => {
|
||||||
@ -464,8 +499,12 @@ export const useScrollToBottom = (currentConversation: IClientConversation) => {
|
|||||||
export const useFetchConversationOnMount = () => {
|
export const useFetchConversationOnMount = () => {
|
||||||
const { conversationId } = useGetChatSearchParams();
|
const { conversationId } = useGetChatSearchParams();
|
||||||
const fetchConversation = useFetchConversation();
|
const fetchConversation = useFetchConversation();
|
||||||
const { currentConversation, addNewestConversation, removeLatestMessage } =
|
const {
|
||||||
useSelectCurrentConversation();
|
currentConversation,
|
||||||
|
addNewestConversation,
|
||||||
|
removeLatestMessage,
|
||||||
|
addNewestAnswer,
|
||||||
|
} = useSelectCurrentConversation();
|
||||||
const ref = useScrollToBottom(currentConversation);
|
const ref = useScrollToBottom(currentConversation);
|
||||||
|
|
||||||
const fetchConversationOnMount = useCallback(() => {
|
const fetchConversationOnMount = useCallback(() => {
|
||||||
@ -483,6 +522,7 @@ export const useFetchConversationOnMount = () => {
|
|||||||
addNewestConversation,
|
addNewestConversation,
|
||||||
ref,
|
ref,
|
||||||
removeLatestMessage,
|
removeLatestMessage,
|
||||||
|
addNewestAnswer,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -504,25 +544,22 @@ export const useHandleMessageInputChange = () => {
|
|||||||
|
|
||||||
export const useSendMessage = (
|
export const useSendMessage = (
|
||||||
conversation: IClientConversation,
|
conversation: IClientConversation,
|
||||||
addNewestConversation: (message: string) => void,
|
addNewestConversation: (message: string, answer?: string) => void,
|
||||||
removeLatestMessage: () => void,
|
removeLatestMessage: () => void,
|
||||||
|
addNewestAnswer: (answer: IAnswer) => void,
|
||||||
) => {
|
) => {
|
||||||
const loading = useOneNamespaceEffectsLoading('chatModel', [
|
|
||||||
'completeConversation',
|
|
||||||
]);
|
|
||||||
const { setConversation } = useSetConversation();
|
const { setConversation } = useSetConversation();
|
||||||
const { conversationId } = useGetChatSearchParams();
|
const { conversationId } = useGetChatSearchParams();
|
||||||
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
|
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
|
||||||
|
|
||||||
const fetchConversation = useFetchConversation();
|
const fetchConversation = useFetchConversation();
|
||||||
const completeConversation = useCompleteConversation();
|
|
||||||
|
|
||||||
const { handleClickConversation } = useClickConversationCard();
|
const { handleClickConversation } = useClickConversationCard();
|
||||||
// const { send } = useConnectWithSseNext();
|
const { send, answer, done } = useSendMessageWithSse();
|
||||||
|
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
async (message: string, id?: string) => {
|
async (message: string, id?: string) => {
|
||||||
const retcode = await completeConversation({
|
const res: Response = await send({
|
||||||
conversation_id: id ?? conversationId,
|
conversation_id: id ?? conversationId,
|
||||||
messages: [
|
messages: [
|
||||||
...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
|
...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
|
||||||
@ -533,27 +570,33 @@ export const useSendMessage = (
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (retcode === 0) {
|
if (res.status === 200) {
|
||||||
if (id) {
|
if (id) {
|
||||||
|
console.info('111');
|
||||||
// new conversation
|
// new conversation
|
||||||
handleClickConversation(id);
|
handleClickConversation(id);
|
||||||
} else {
|
} else {
|
||||||
fetchConversation(conversationId);
|
console.info('222');
|
||||||
|
// fetchConversation(conversationId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.info('333');
|
||||||
|
|
||||||
// cancel loading
|
// cancel loading
|
||||||
setValue(message);
|
setValue(message);
|
||||||
|
console.info('removeLatestMessage111');
|
||||||
removeLatestMessage();
|
removeLatestMessage();
|
||||||
}
|
}
|
||||||
|
console.info('false');
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
conversation?.message,
|
conversation?.message,
|
||||||
conversationId,
|
conversationId,
|
||||||
fetchConversation,
|
// fetchConversation,
|
||||||
handleClickConversation,
|
handleClickConversation,
|
||||||
removeLatestMessage,
|
removeLatestMessage,
|
||||||
setValue,
|
setValue,
|
||||||
completeConversation,
|
send,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -572,19 +615,27 @@ export const useSendMessage = (
|
|||||||
[conversationId, setConversation, sendMessage],
|
[conversationId, setConversation, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePressEnter = () => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (answer.answer) {
|
||||||
|
addNewestAnswer(answer);
|
||||||
|
console.info('true?');
|
||||||
|
console.info('send msg:', answer.answer);
|
||||||
|
}
|
||||||
|
}, [answer, addNewestAnswer]);
|
||||||
|
|
||||||
|
const handlePressEnter = useCallback(() => {
|
||||||
|
if (done) {
|
||||||
setValue('');
|
setValue('');
|
||||||
addNewestConversation(value);
|
|
||||||
handleSendMessage(value.trim());
|
handleSendMessage(value.trim());
|
||||||
}
|
}
|
||||||
};
|
addNewestConversation(value);
|
||||||
|
}, [addNewestConversation, handleSendMessage, done, setValue, value]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handlePressEnter,
|
handlePressEnter,
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
value,
|
value,
|
||||||
loading,
|
loading: !done,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IConversation, Message } from '@/interfaces/database/chat';
|
import { IConversation, IReference, Message } from '@/interfaces/database/chat';
|
||||||
import { FormInstance } from 'antd';
|
import { FormInstance } from 'antd';
|
||||||
|
|
||||||
export interface ISegmentedContentProps {
|
export interface ISegmentedContentProps {
|
||||||
@ -24,6 +24,7 @@ export type IPromptConfigParameters = Omit<VariableTableDataType, 'variable'>;
|
|||||||
|
|
||||||
export interface IMessage extends Message {
|
export interface IMessage extends Message {
|
||||||
id: string;
|
id: string;
|
||||||
|
reference?: IReference; // the latest news has reference
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IClientConversation extends IConversation {
|
export interface IClientConversation extends IConversation {
|
||||||
|
@ -23,3 +23,23 @@
|
|||||||
.referenceIcon {
|
.referenceIcon {
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: black;
|
||||||
|
animation: blink 0.6s infinite;
|
||||||
|
vertical-align: text-top;
|
||||||
|
@keyframes blink {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ import { visitParents } from 'unist-util-visit-parents';
|
|||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
|
||||||
const reg = /(#{2}\d+\${2})/g;
|
const reg = /(#{2}\d+\${2})/g;
|
||||||
|
const curReg = /(~{2}\d+\${2})/g;
|
||||||
|
|
||||||
const getChunkIndex = (match: string) => Number(match.slice(2, -2));
|
const getChunkIndex = (match: string) => Number(match.slice(2, -2));
|
||||||
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
// TODO: The display of the table is inconsistent with the display previously placed in the MessageItem.
|
||||||
@ -61,7 +62,7 @@ const MarkdownContent = ({
|
|||||||
(chunkIndex: number) => {
|
(chunkIndex: number) => {
|
||||||
const chunks = reference?.chunks ?? [];
|
const chunks = reference?.chunks ?? [];
|
||||||
const chunkItem = chunks[chunkIndex];
|
const chunkItem = chunks[chunkIndex];
|
||||||
const document = reference?.doc_aggs.find(
|
const document = reference?.doc_aggs?.find(
|
||||||
(x) => x?.doc_id === chunkItem?.doc_id,
|
(x) => x?.doc_id === chunkItem?.doc_id,
|
||||||
);
|
);
|
||||||
const documentId = document?.doc_id;
|
const documentId = document?.doc_id;
|
||||||
@ -129,7 +130,7 @@ const MarkdownContent = ({
|
|||||||
|
|
||||||
const renderReference = useCallback(
|
const renderReference = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
return reactStringReplace(text, reg, (match, i) => {
|
let replacedText = reactStringReplace(text, reg, (match, i) => {
|
||||||
const chunkIndex = getChunkIndex(match);
|
const chunkIndex = getChunkIndex(match);
|
||||||
return (
|
return (
|
||||||
<Popover content={getPopoverContent(chunkIndex)}>
|
<Popover content={getPopoverContent(chunkIndex)}>
|
||||||
@ -137,6 +138,12 @@ const MarkdownContent = ({
|
|||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
replacedText = reactStringReplace(replacedText, curReg, (match, i) => (
|
||||||
|
<span className={styles.cursor} key={i}></span>
|
||||||
|
));
|
||||||
|
|
||||||
|
return replacedText;
|
||||||
},
|
},
|
||||||
[getPopoverContent],
|
[getPopoverContent],
|
||||||
);
|
);
|
||||||
|
@ -1,51 +1,11 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
useCreateSharedConversationOnMount,
|
|
||||||
useSelectCurrentSharedConversation,
|
|
||||||
useSendSharedMessage,
|
|
||||||
} from '../shared-hooks';
|
|
||||||
import ChatContainer from './large';
|
import ChatContainer from './large';
|
||||||
|
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
|
||||||
const SharedChat = () => {
|
const SharedChat = () => {
|
||||||
const { conversationId } = useCreateSharedConversationOnMount();
|
|
||||||
const {
|
|
||||||
currentConversation,
|
|
||||||
addNewestConversation,
|
|
||||||
removeLatestMessage,
|
|
||||||
ref,
|
|
||||||
loading,
|
|
||||||
setCurrentConversation,
|
|
||||||
} = useSelectCurrentSharedConversation(conversationId);
|
|
||||||
|
|
||||||
const {
|
|
||||||
handlePressEnter,
|
|
||||||
handleInputChange,
|
|
||||||
value,
|
|
||||||
loading: sendLoading,
|
|
||||||
} = useSendSharedMessage(
|
|
||||||
currentConversation,
|
|
||||||
addNewestConversation,
|
|
||||||
removeLatestMessage,
|
|
||||||
setCurrentConversation,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.info(location.href);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chatWrapper}>
|
<div className={styles.chatWrapper}>
|
||||||
<ChatContainer
|
<ChatContainer></ChatContainer>
|
||||||
value={value}
|
|
||||||
handleInputChange={handleInputChange}
|
|
||||||
handlePressEnter={handlePressEnter}
|
|
||||||
loading={loading}
|
|
||||||
sendLoading={sendLoading}
|
|
||||||
conversation={currentConversation}
|
|
||||||
ref={ref}
|
|
||||||
></ChatContainer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,50 @@
|
|||||||
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
||||||
import { MessageType } from '@/constants/chat';
|
import { MessageType } from '@/constants/chat';
|
||||||
import { useTranslate } from '@/hooks/commonHooks';
|
import { useTranslate } from '@/hooks/commonHooks';
|
||||||
import { Message } from '@/interfaces/database/chat';
|
import { IReference, Message } from '@/interfaces/database/chat';
|
||||||
import { Avatar, Button, Flex, Input, Skeleton, Spin } from 'antd';
|
import { Avatar, Button, Flex, Input, List, Spin } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useSelectConversationLoading } from '../hooks';
|
|
||||||
|
|
||||||
import HightLightMarkdown from '@/components/highlight-markdown';
|
import NewDocumentLink from '@/components/new-document-link';
|
||||||
import React, { ChangeEventHandler, forwardRef } from 'react';
|
import SvgIcon from '@/components/svg-icon';
|
||||||
import { IClientConversation } from '../interface';
|
import { useGetDocumentUrl } from '@/hooks/documentHooks';
|
||||||
|
import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
|
||||||
|
import { getExtension, isPdf } from '@/utils/documentUtils';
|
||||||
|
import { forwardRef, useMemo } from 'react';
|
||||||
|
import MarkdownContent from '../markdown-content';
|
||||||
|
import {
|
||||||
|
useCreateSharedConversationOnMount,
|
||||||
|
useSelectCurrentSharedConversation,
|
||||||
|
useSendSharedMessage,
|
||||||
|
} from '../shared-hooks';
|
||||||
|
import { buildMessageItemReference } from '../utils';
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
|
|
||||||
const MessageItem = ({ item }: { item: Message }) => {
|
const MessageItem = ({
|
||||||
|
item,
|
||||||
|
reference,
|
||||||
|
loading = false,
|
||||||
|
}: {
|
||||||
|
item: Message;
|
||||||
|
reference: IReference;
|
||||||
|
loading?: boolean;
|
||||||
|
}) => {
|
||||||
const isAssistant = item.role === MessageType.Assistant;
|
const isAssistant = item.role === MessageType.Assistant;
|
||||||
|
const { t } = useTranslate('chat');
|
||||||
|
const fileThumbnails = useSelectFileThumbnails();
|
||||||
|
const getDocumentUrl = useGetDocumentUrl();
|
||||||
|
|
||||||
|
const referenceDocumentList = useMemo(() => {
|
||||||
|
return reference?.doc_aggs ?? [];
|
||||||
|
}, [reference?.doc_aggs]);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
let text = item.content;
|
||||||
|
if (text === '') {
|
||||||
|
text = t('searching');
|
||||||
|
}
|
||||||
|
return loading ? text?.concat('~~2$$') : text;
|
||||||
|
}, [item.content, loading, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -45,12 +77,43 @@ const MessageItem = ({ item }: { item: Message }) => {
|
|||||||
<Flex vertical gap={8} flex={1}>
|
<Flex vertical gap={8} flex={1}>
|
||||||
<b>{isAssistant ? '' : 'You'}</b>
|
<b>{isAssistant ? '' : 'You'}</b>
|
||||||
<div className={styles.messageText}>
|
<div className={styles.messageText}>
|
||||||
{item.content !== '' ? (
|
<MarkdownContent
|
||||||
<HightLightMarkdown>{item.content}</HightLightMarkdown>
|
reference={reference}
|
||||||
) : (
|
clickDocumentButton={() => {}}
|
||||||
<Skeleton active className={styles.messageEmpty} />
|
content={content}
|
||||||
)}
|
></MarkdownContent>
|
||||||
</div>
|
</div>
|
||||||
|
{isAssistant && referenceDocumentList.length > 0 && (
|
||||||
|
<List
|
||||||
|
bordered
|
||||||
|
dataSource={referenceDocumentList}
|
||||||
|
renderItem={(item) => {
|
||||||
|
const fileThumbnail = fileThumbnails[item.doc_id];
|
||||||
|
const fileExtension = getExtension(item.doc_name);
|
||||||
|
return (
|
||||||
|
<List.Item>
|
||||||
|
<Flex gap={'small'} align="center">
|
||||||
|
{fileThumbnail ? (
|
||||||
|
<img src={fileThumbnail}></img>
|
||||||
|
) : (
|
||||||
|
<SvgIcon
|
||||||
|
name={`file-icon/${fileExtension}`}
|
||||||
|
width={24}
|
||||||
|
></SvgIcon>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NewDocumentLink
|
||||||
|
link={getDocumentUrl(item.doc_id)}
|
||||||
|
preventDefault={!isPdf(item.doc_name)}
|
||||||
|
>
|
||||||
|
{item.doc_name}
|
||||||
|
</NewDocumentLink>
|
||||||
|
</Flex>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -58,28 +121,31 @@ const MessageItem = ({ item }: { item: Message }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IProps {
|
const ChatContainer = () => {
|
||||||
handlePressEnter(): void;
|
const { t } = useTranslate('chat');
|
||||||
handleInputChange: ChangeEventHandler<HTMLInputElement>;
|
const { conversationId } = useCreateSharedConversationOnMount();
|
||||||
value: string;
|
const {
|
||||||
loading: boolean;
|
currentConversation: conversation,
|
||||||
sendLoading: boolean;
|
addNewestConversation,
|
||||||
conversation: IClientConversation;
|
removeLatestMessage,
|
||||||
ref: React.LegacyRef<any>;
|
ref,
|
||||||
}
|
loading,
|
||||||
|
setCurrentConversation,
|
||||||
|
addNewestAnswer,
|
||||||
|
} = useSelectCurrentSharedConversation(conversationId);
|
||||||
|
|
||||||
const ChatContainer = (
|
const {
|
||||||
{
|
|
||||||
handlePressEnter,
|
handlePressEnter,
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
value,
|
value,
|
||||||
loading: sendLoading,
|
loading: sendLoading,
|
||||||
|
} = useSendSharedMessage(
|
||||||
conversation,
|
conversation,
|
||||||
}: IProps,
|
addNewestConversation,
|
||||||
ref: React.LegacyRef<any>,
|
removeLatestMessage,
|
||||||
) => {
|
setCurrentConversation,
|
||||||
const loading = useSelectConversationLoading();
|
addNewestAnswer,
|
||||||
const { t } = useTranslate('chat');
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -87,9 +153,18 @@ const ChatContainer = (
|
|||||||
<Flex flex={1} vertical className={styles.messageContainer}>
|
<Flex flex={1} vertical className={styles.messageContainer}>
|
||||||
<div>
|
<div>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{conversation?.message?.map((message) => {
|
{conversation?.message?.map((message, i) => {
|
||||||
return (
|
return (
|
||||||
<MessageItem key={message.id} item={message}></MessageItem>
|
<MessageItem
|
||||||
|
key={message.id}
|
||||||
|
item={message}
|
||||||
|
reference={buildMessageItemReference(conversation, message)}
|
||||||
|
loading={
|
||||||
|
message.role === MessageType.Assistant &&
|
||||||
|
sendLoading &&
|
||||||
|
conversation?.message.length - 1 === i
|
||||||
|
}
|
||||||
|
></MessageItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Spin>
|
</Spin>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { MessageType } from '@/constants/chat';
|
import { MessageType } from '@/constants/chat';
|
||||||
import {
|
import {
|
||||||
useCompleteSharedConversation,
|
|
||||||
useCreateSharedConversation,
|
useCreateSharedConversation,
|
||||||
useFetchSharedConversation,
|
useFetchSharedConversation,
|
||||||
} from '@/hooks/chatHooks';
|
} from '@/hooks/chatHooks';
|
||||||
|
import { useSendMessageWithSse } from '@/hooks/logicHooks';
|
||||||
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
||||||
|
import { IAnswer } from '@/interfaces/database/chat';
|
||||||
|
import api from '@/utils/api';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
import {
|
import {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@ -76,6 +78,27 @@ export const useSelectCurrentSharedConversation = (conversationId: string) => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const addNewestAnswer = useCallback((answer: IAnswer) => {
|
||||||
|
setCurrentConversation((pre) => {
|
||||||
|
const latestMessage = pre.message?.at(-1);
|
||||||
|
|
||||||
|
if (latestMessage) {
|
||||||
|
return {
|
||||||
|
...pre,
|
||||||
|
message: [
|
||||||
|
...pre.message.slice(0, -1),
|
||||||
|
{
|
||||||
|
...latestMessage,
|
||||||
|
content: answer.answer,
|
||||||
|
reference: answer.reference,
|
||||||
|
} as IMessage,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return pre;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const removeLatestMessage = useCallback(() => {
|
const removeLatestMessage = useCallback(() => {
|
||||||
setCurrentConversation((pre) => {
|
setCurrentConversation((pre) => {
|
||||||
const nextMessages = pre.message.slice(0, -2);
|
const nextMessages = pre.message.slice(0, -2);
|
||||||
@ -106,6 +129,7 @@ export const useSelectCurrentSharedConversation = (conversationId: string) => {
|
|||||||
loading,
|
loading,
|
||||||
ref,
|
ref,
|
||||||
setCurrentConversation,
|
setCurrentConversation,
|
||||||
|
addNewestAnswer,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,20 +138,19 @@ export const useSendSharedMessage = (
|
|||||||
addNewestConversation: (message: string) => void,
|
addNewestConversation: (message: string) => void,
|
||||||
removeLatestMessage: () => void,
|
removeLatestMessage: () => void,
|
||||||
setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>,
|
setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>,
|
||||||
|
addNewestAnswer: (answer: IAnswer) => void,
|
||||||
) => {
|
) => {
|
||||||
const conversationId = conversation.id;
|
const conversationId = conversation.id;
|
||||||
const loading = useOneNamespaceEffectsLoading('chatModel', [
|
|
||||||
'completeExternalConversation',
|
|
||||||
]);
|
|
||||||
const setConversation = useCreateSharedConversation();
|
const setConversation = useCreateSharedConversation();
|
||||||
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
|
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
|
||||||
|
|
||||||
const fetchConversation = useFetchSharedConversation();
|
const { send, answer, done } = useSendMessageWithSse(
|
||||||
const completeConversation = useCompleteSharedConversation();
|
api.completeExternalConversation,
|
||||||
|
);
|
||||||
|
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
async (message: string, id?: string) => {
|
async (message: string, id?: string) => {
|
||||||
const retcode = await completeConversation({
|
const res: Response = await send({
|
||||||
conversation_id: id ?? conversationId,
|
conversation_id: id ?? conversationId,
|
||||||
quote: false,
|
quote: false,
|
||||||
messages: [
|
messages: [
|
||||||
@ -139,11 +162,11 @@ export const useSendSharedMessage = (
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (retcode === 0) {
|
if (res?.status === 200) {
|
||||||
const data = await fetchConversation(conversationId);
|
// const data = await fetchConversation(conversationId);
|
||||||
if (data.retcode === 0) {
|
// if (data.retcode === 0) {
|
||||||
setCurrentConversation(data.data);
|
// setCurrentConversation(data.data);
|
||||||
}
|
// }
|
||||||
} else {
|
} else {
|
||||||
// cancel loading
|
// cancel loading
|
||||||
setValue(message);
|
setValue(message);
|
||||||
@ -153,11 +176,11 @@ export const useSendSharedMessage = (
|
|||||||
[
|
[
|
||||||
conversationId,
|
conversationId,
|
||||||
conversation?.message,
|
conversation?.message,
|
||||||
fetchConversation,
|
// fetchConversation,
|
||||||
removeLatestMessage,
|
removeLatestMessage,
|
||||||
setValue,
|
setValue,
|
||||||
completeConversation,
|
send,
|
||||||
setCurrentConversation,
|
// setCurrentConversation,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -176,18 +199,24 @@ export const useSendSharedMessage = (
|
|||||||
[conversationId, setConversation, sendMessage],
|
[conversationId, setConversation, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePressEnter = () => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (answer.answer) {
|
||||||
|
addNewestAnswer(answer);
|
||||||
|
}
|
||||||
|
}, [answer, addNewestAnswer]);
|
||||||
|
|
||||||
|
const handlePressEnter = useCallback(() => {
|
||||||
|
if (done) {
|
||||||
setValue('');
|
setValue('');
|
||||||
addNewestConversation(value);
|
addNewestConversation(value);
|
||||||
handleSendMessage(value.trim());
|
handleSendMessage(value.trim());
|
||||||
}
|
}
|
||||||
};
|
}, [addNewestConversation, done, handleSendMessage, setValue, value]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handlePressEnter,
|
handlePressEnter,
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
value,
|
value,
|
||||||
loading,
|
loading: !done,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import { MessageType } from '@/constants/chat';
|
||||||
import { IConversation, IReference } from '@/interfaces/database/chat';
|
import { IConversation, IReference } from '@/interfaces/database/chat';
|
||||||
import { EmptyConversationId, variableEnabledFieldMap } from './constants';
|
import { EmptyConversationId, variableEnabledFieldMap } from './constants';
|
||||||
|
import { IClientConversation, IMessage } from './interface';
|
||||||
|
|
||||||
export const excludeUnEnabledVariables = (values: any) => {
|
export const excludeUnEnabledVariables = (values: any) => {
|
||||||
const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> =
|
const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> =
|
||||||
@ -20,7 +22,7 @@ export const getDocumentIdsFromConversionReference = (data: IConversation) => {
|
|||||||
const documentIds = data.reference.reduce(
|
const documentIds = data.reference.reduce(
|
||||||
(pre: Array<string>, cur: IReference) => {
|
(pre: Array<string>, cur: IReference) => {
|
||||||
cur.doc_aggs
|
cur.doc_aggs
|
||||||
.map((x) => x.doc_id)
|
?.map((x) => x.doc_id)
|
||||||
.forEach((x) => {
|
.forEach((x) => {
|
||||||
if (pre.every((y) => y !== x)) {
|
if (pre.every((y) => y !== x)) {
|
||||||
pre.push(x);
|
pre.push(x);
|
||||||
@ -32,3 +34,20 @@ export const getDocumentIdsFromConversionReference = (data: IConversation) => {
|
|||||||
);
|
);
|
||||||
return documentIds.join(',');
|
return documentIds.join(',');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const buildMessageItemReference = (
|
||||||
|
conversation: IClientConversation,
|
||||||
|
message: IMessage,
|
||||||
|
) => {
|
||||||
|
const assistantMessages = conversation.message
|
||||||
|
?.filter((x) => x.role === MessageType.Assistant)
|
||||||
|
.slice(1);
|
||||||
|
const referenceIndex = assistantMessages.findIndex(
|
||||||
|
(x) => x.id === message.id,
|
||||||
|
);
|
||||||
|
const reference = message?.reference
|
||||||
|
? message?.reference
|
||||||
|
: conversation.reference[referenceIndex];
|
||||||
|
|
||||||
|
return reference;
|
||||||
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Authorization, Token, UserInfo } from '@/constants/authorization';
|
import { Authorization, Token, UserInfo } from '@/constants/authorization';
|
||||||
|
import { getSearchValue } from './commonUtil';
|
||||||
const KeySet = [Authorization, Token, UserInfo];
|
const KeySet = [Authorization, Token, UserInfo];
|
||||||
|
|
||||||
const storage = {
|
const storage = {
|
||||||
@ -21,7 +21,7 @@ const storage = {
|
|||||||
setToken: (value: string) => {
|
setToken: (value: string) => {
|
||||||
localStorage.setItem(Token, value);
|
localStorage.setItem(Token, value);
|
||||||
},
|
},
|
||||||
setUserInfo: (value: string | Object) => {
|
setUserInfo: (value: string | Record<string, unknown>) => {
|
||||||
let valueStr = typeof value !== 'string' ? JSON.stringify(value) : value;
|
let valueStr = typeof value !== 'string' ? JSON.stringify(value) : value;
|
||||||
localStorage.setItem(UserInfo, valueStr);
|
localStorage.setItem(UserInfo, valueStr);
|
||||||
},
|
},
|
||||||
@ -46,4 +46,13 @@ const storage = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAuthorization = () => {
|
||||||
|
const sharedId = getSearchValue('shared_id');
|
||||||
|
const authorization = sharedId
|
||||||
|
? 'Bearer ' + sharedId
|
||||||
|
: storage.getAuthorization() || '';
|
||||||
|
|
||||||
|
return authorization;
|
||||||
|
};
|
||||||
|
|
||||||
export default storage;
|
export default storage;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Authorization } from '@/constants/authorization';
|
import { Authorization } from '@/constants/authorization';
|
||||||
import i18n from '@/locales/config';
|
import i18n from '@/locales/config';
|
||||||
import authorizationUtil from '@/utils/authorizationUtil';
|
import authorizationUtil, { getAuthorization } from '@/utils/authorizationUtil';
|
||||||
import { message, notification } from 'antd';
|
import { message, notification } from 'antd';
|
||||||
import { history } from 'umi';
|
import { history } from 'umi';
|
||||||
import { RequestMethod, extend } from 'umi-request';
|
import { RequestMethod, extend } from 'umi-request';
|
||||||
import { convertTheKeysOfTheObjectToSnake, getSearchValue } from './commonUtil';
|
import { convertTheKeysOfTheObjectToSnake } from './commonUtil';
|
||||||
|
|
||||||
const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message
|
const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.';
|
||||||
|
|
||||||
const RetcodeMessage = {
|
const RetcodeMessage = {
|
||||||
200: i18n.t('message.200'),
|
200: i18n.t('message.200'),
|
||||||
@ -41,9 +41,7 @@ type ResultCode =
|
|||||||
| 502
|
| 502
|
||||||
| 503
|
| 503
|
||||||
| 504;
|
| 504;
|
||||||
/**
|
|
||||||
* 异常处理程序
|
|
||||||
*/
|
|
||||||
interface ResponseType {
|
interface ResponseType {
|
||||||
retcode: number;
|
retcode: number;
|
||||||
data: any;
|
data: any;
|
||||||
@ -55,7 +53,6 @@ const errorHandler = (error: {
|
|||||||
message: string;
|
message: string;
|
||||||
}): Response => {
|
}): Response => {
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
// 手动中断请求 abort
|
|
||||||
if (error.message === ABORT_REQUEST_ERR_MESSAGE) {
|
if (error.message === ABORT_REQUEST_ERR_MESSAGE) {
|
||||||
console.log('user abort request');
|
console.log('user abort request');
|
||||||
} else {
|
} else {
|
||||||
@ -77,20 +74,13 @@ const errorHandler = (error: {
|
|||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置request请求时的默认参数
|
|
||||||
*/
|
|
||||||
const request: RequestMethod = extend({
|
const request: RequestMethod = extend({
|
||||||
errorHandler, // 默认错误处理
|
errorHandler,
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
getResponse: true,
|
getResponse: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
request.interceptors.request.use((url: string, options: any) => {
|
request.interceptors.request.use((url: string, options: any) => {
|
||||||
const sharedId = getSearchValue('shared_id');
|
|
||||||
const authorization = sharedId
|
|
||||||
? 'Bearer ' + sharedId
|
|
||||||
: authorizationUtil.getAuthorization();
|
|
||||||
const data = convertTheKeysOfTheObjectToSnake(options.data);
|
const data = convertTheKeysOfTheObjectToSnake(options.data);
|
||||||
const params = convertTheKeysOfTheObjectToSnake(options.params);
|
const params = convertTheKeysOfTheObjectToSnake(options.params);
|
||||||
|
|
||||||
@ -101,7 +91,9 @@ request.interceptors.request.use((url: string, options: any) => {
|
|||||||
data,
|
data,
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
...(options.skipToken ? undefined : { [Authorization]: authorization }),
|
...(options.skipToken
|
||||||
|
? undefined
|
||||||
|
: { [Authorization]: getAuthorization() }),
|
||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
},
|
||||||
interceptors: true,
|
interceptors: true,
|
||||||
@ -109,16 +101,11 @@ request.interceptors.request.use((url: string, options: any) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
* 请求response拦截器
|
|
||||||
* */
|
|
||||||
|
|
||||||
request.interceptors.response.use(async (response: any, options) => {
|
request.interceptors.response.use(async (response: any, options) => {
|
||||||
if (options.responseType === 'blob') {
|
if (options.responseType === 'blob') {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
const data: ResponseType = await response.clone().json();
|
const data: ResponseType = await response.clone().json();
|
||||||
// response 拦截
|
|
||||||
|
|
||||||
if (data.retcode === 401 || data.retcode === 401) {
|
if (data.retcode === 401 || data.retcode === 401) {
|
||||||
notification.error({
|
notification.error({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user