general-assistant
Version:
General Assistant components
583 lines (582 loc) • 27.1 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useTranslation } from 'react-i18next';
import Chat, { useMessages } from 'datastellar-chatui';
import { defaultsDeep, extend, isArray, debounce } from 'lodash';
import { useEffect, useRef, useState, useContext } from 'react';
import { LuLoader } from "react-icons/lu";
import { sendRequestWithConfig } from '../_utils/tools';
import { useStore } from 'zustand';
import StoreContext from '../_store/StoreContext';
import { RandomString, isJSON } from '../_utils/tools';
import defComposerInput from '../Composer/index';
import MessageCard from '../MessageCard/index';
import TypeCard from '../TypeCard/index';
import './index.css';
import classNames from 'classnames';
import AreaRight from '../AreaRight';
let myController = null;
let lastScrollTop = 0;
export default function ChatBox({ center }) {
const { t } = useTranslation();
const { composer } = center;
const dataStore = useContext(StoreContext);
const { updateChatCtxs, MyHandlers, GlobalCtxs, getRequestConfig, getConfig, getAiAvatar, getAiName, getUserAvatar, getUserName, getChatMessages, setChatMessages, myHistoryList, topic, isOpenFirst, myTopicOpened, hasMoreUp, setHasMoreUp, hasMoreDown, setHasMoreDown, showAreaLeft, loadAreaRight, showAreaRight, findHistoryMsg, updateHistoryMsg, historyComposer, updateHistoryComposer, } = useStore(dataStore, (state) => ({
updateChatCtxs: state.updateChatCtxs,
MyHandlers: state.MyHandlers,
GlobalCtxs: state.GlobalCtxs,
getRequestConfig: state.getRequestConfig,
getConfig: state.getConfig,
getAiAvatar: state.getAiAvatar,
getAiName: state.getAiName,
getUserAvatar: state.getUserAvatar,
getUserName: state.getUserName,
getChatMessages: state.getChatMessages,
setChatMessages: state.setChatMessages,
myHistoryList: state.myHistoryList,
topic: state.topic,
isOpenFirst: state.isOpenFirst,
myTopicOpened: state.myTopicOpened,
hasMoreUp: state.hasMoreUp,
setHasMoreUp: state.setHasMoreUp,
hasMoreDown: state.hasMoreDown,
setHasMoreDown: state.setHasMoreDown,
showAreaLeft: state.showAreaLeft,
loadAreaRight: state.loadAreaRight,
showAreaRight: state.showAreaRight,
findHistoryMsg: state.findHistoryMsg,
updateHistoryMsg: state.updateHistoryMsg,
historyComposer: state.historyComposer,
updateHistoryComposer: state.updateHistoryComposer,
}));
const { renderMessageContent, onScrollTo, beforeSendMessage, sendMessage, beforeAppendMessage, onBreakQA, onAfterOpenChat, onBeforeOpenChat, } = MyHandlers;
const [myPattern, setMyPattern] = useState('send');
const _myComposerRef = useRef(null);
const _myChatContainer = useRef(null);
const _myMsgContainer = useRef(null);
const { defaultMessages, robot, user, autoResetMessages } = getConfig();
const { timeout, method, headers, QAChatFetch: reqQAChatFetch, QAChatJson: reqQAChatJson, history: reqHistoryMsg, } = getRequestConfig();
const [myRobot, setMyRobot] = useState({ avatar: getAiAvatar(), name: getAiName() });
const [myUser, setMyUser] = useState({ avatar: getUserAvatar(), name: getUserName() });
useEffect(() => {
setMyRobot({ avatar: getAiAvatar(), name: getAiName() });
setMyUser({ avatar: getUserAvatar(), name: getUserName() });
}, [robot, user]);
const myComposerEType = composer !== undefined ? composer : defComposerInput;
const { messages, resetList, appendMsg, updateMsg, deleteMsg, prependMsgs } = useMessages(dealDefaultMsg());
const [myTaskId, setMyTaskId] = useState('');
const MyBeforeSendMessage = (msg, ctx) => typeof beforeSendMessage === 'function' ? beforeSendMessage(msg, ctx) : msg;
const MySendMessage = (inVal, ctx) => (typeof sendMessage === 'function' ? sendMessage(inVal, ctx) : inVal);
const MyBeforeAppendMessage = (msg, ctx) => typeof beforeAppendMessage === 'function' ? beforeAppendMessage(msg, ctx) : msg;
const MyOnScrollTo = (ev, ctx, direction) => (typeof onScrollTo === 'function') ? onScrollTo(ev, ctx, direction) : requestHistoryTo(direction);
const [scrollLoading, setScrollLoading] = useState({ status: 'succeed' });
function appendMsgFn(posi, msg, toScroll, direct) {
if (!posi)
return;
const { _id, type, content } = msg || {};
if (!type || !content)
return;
const t_id = _id !== undefined ? _id : RandomString();
const _find = getChatMessages()?.find((e) => e._id === t_id);
if (_find)
return;
let t_msg = Object.assign({}, msg, { _id: t_id, position: posi });
if (type !== 'System' && !msg.user) {
t_msg = Object.assign({}, t_msg, { user: posi === 'left' ? myRobot : myUser });
}
const _dt_msg = MyBeforeAppendMessage(t_msg, GlobalCtxs);
if (_dt_msg === false)
return;
if (direct === 'up') {
prependMsgs([_dt_msg]);
if (toScroll === true)
scrollToTop();
}
else {
appendMsg(_dt_msg);
if (toScroll === true)
scrollToEnd();
}
}
function scrollToEnd(_opt) {
const { delay = 500, force = true, animated } = _opt || {};
setTimeout(() => {
_myMsgContainer?.current?.scrollToEnd({ force, animated });
}, delay);
}
function scrollToTop() {
}
function setText(text) {
return _myComposerRef.current?.ele.setText(text);
}
function breakRequest() {
if (typeof onBreakQA === 'function') {
const { next, isAbort } = onBreakQA({ data: { taskid: myTaskId, controller: myController }, ctx: GlobalCtxs });
if (!next) {
if (isAbort)
setMyPattern('send');
return;
}
}
setMyPattern('send');
if (myController && myController.abort) {
myController.abort();
myController = null;
}
}
function updateTypeMsg({ _id, content, type, posi }) {
const _oldmsg = getChatMessages().find((_e) => _e._id === _id);
if (_oldmsg === undefined || (type === undefined && posi === undefined)) {
return;
}
const _newMsg = {
_id,
type: type || _oldmsg.type,
position: posi || _oldmsg.position,
content: defaultsDeep(content, _oldmsg.content),
};
let t_msg = defaultsDeep(_newMsg, _oldmsg);
if (type !== 'System' && posi)
t_msg = Object.assign(t_msg, { user: posi === 'left' ? myRobot : myUser });
updateMsg(_id, t_msg);
return t_msg;
}
function isStandardFetchMsgObject(value = '') {
if (value.search('data:') === -1)
return false;
if (value.search('event') === -1)
return false;
if (value.search('task_id') === -1)
return false;
return true;
}
function isStandardChatResponse(res) {
const { data } = res || {};
if (data.msgList === undefined)
return false;
if (!Array.isArray(data.msgList))
return false;
if (data.msgList.length > 0) {
const _json = JSON.stringify(data.msgList);
if (_json.search('cardId') !== -1)
return false;
}
return true;
}
function isCardCont(_str) {
if (isJSON(_str)) {
const _obj = JSON.parse(_str);
if (!_obj.cardId)
return false;
}
return false;
}
function defParseResponse(response) {
const { data } = response || {};
return {
showType: 1,
chatEnable: true,
errorMessage: '',
msglist: [
{
cardId: 'Text',
data: '默认文字',
},
],
source: ['各类金融分析的优缺点分析', '四大金融分析方法论'],
quickReplies: [
{ name: '在金融市场分析文章中加入一些数据图表来展示分析结果' },
{ name: '以《金融市场分析之法的长短板》为题写一篇1000字左右的金融市场分析文章' },
],
};
}
function simulationAnswer(_opednid, _id, text) {
setTimeout(() => {
updateTypeMsg({ _id, content: { data: text }, type: 'Text' });
updateHistoryMsg(_opednid, { _id, content: { data: text }, type: 'Text' });
updateHistoryComposer(_opednid, { controller: null });
}, 5000);
}
async function dealRequest_FETCH(_id, text) {
const OpenedId = myTopicOpened;
if (typeof reqQAChatJson !== 'function' && typeof reqQAChatFetch !== 'function') {
simulationAnswer(OpenedId, _id, text);
return;
}
const _thisController = new AbortController();
myController = _thisController;
updateHistoryComposer(OpenedId, { controller: _thisController });
if (typeof reqQAChatJson === 'function') {
const _reqConfigJ = reqQAChatJson({ data: text, ctx: GlobalCtxs });
if (!_reqConfigJ.url) {
simulationAnswer(OpenedId, _id, text);
return;
}
sendRequestWithConfig({
commonOpt: { timeout, method, headers },
parameter: _reqConfigJ,
isFecth: false,
controller: _thisController,
onResponse(response) {
if (response.status === 200) {
let chatObject = null;
if (!isStandardChatResponse(response)) {
console.log('isStandardChatResponse', '检查数据json Response格式转换');
return;
}
if (chatObject && chatObject.msglist) {
let _comb = Object.assign({}, chatObject, { data: chatObject.msglist });
delete chatObject.msglist;
const _msg01 = { _id, content: { data: _comb }, type: 'Combine' };
updateTypeMsg(_msg01);
updateHistoryMsg(OpenedId, _msg01);
}
}
else {
const _msg01err = { _id, content: { data: `${response.status} ${response.statusText}` }, type: 'Text' };
updateTypeMsg(_msg01err);
updateHistoryMsg(OpenedId, _msg01err);
}
},
onCatch(err) {
const _msg01err02 = { _id, content: { data: err.toString() }, type: 'Text' };
updateTypeMsg(_msg01err02);
updateHistoryMsg(OpenedId, _msg01err02);
},
onFinally() {
console.log('reqQAChatJson onFinally');
updateHistoryComposer(OpenedId, { controller: null });
},
});
}
else if (typeof reqQAChatFetch === 'function') {
const _reqConfigF = reqQAChatFetch({ data: { text }, ctx: GlobalCtxs });
if (!_reqConfigF.url) {
simulationAnswer(OpenedId, _id, text);
return;
}
let retStr = '';
let answerStr = '';
let valueStr = '';
let cardType = 'Markdown';
sendRequestWithConfig({
commonOpt: { timeout, method, headers },
parameter: _reqConfigF,
isFecth: true,
controller: _thisController,
onResponse(textValue) {
if (!isStandardFetchMsgObject(textValue)) {
console.log('isStandardFetchMsgObject', '检查数据fetch Response格式转换');
return;
}
let resultObj = textValue.split('\n');
resultObj.forEach((value1, index, array) => {
if (!value1)
return;
value1.startsWith('data:') ? (valueStr = value1) : (valueStr += value1);
let valueObject = null;
try {
valueObject = JSON.parse(valueStr.substring('data:'.length));
}
catch (error) {
console.log('FetchMsgObject JSON 解析失败 跳过', valueStr);
valueObject = {};
}
if (!myTaskId) {
setMyTaskId(valueObject.task_id);
}
let text = '';
const eventName = valueObject.event;
if (eventName === 'message') {
if (valueObject.status === 400) {
text = valueObject.message;
}
else {
text = valueObject.answer || '';
}
answerStr += text;
retStr += text;
if (isCardCont(retStr)) {
cardType = 'Combine';
const _msg02 = { _id, content: { data: JSON.parse(retStr) }, type: cardType };
updateHistoryMsg(OpenedId, _msg02);
}
else {
cardType = 'Markdown';
const _msg03 = { _id, content: { data: retStr }, type: cardType, OpenedId };
updateHistoryMsg(OpenedId, _msg03);
}
valueStr = '';
}
else if (eventName === 'message_end') {
}
else if (eventName === 'error') {
cardType = 'Text';
const _msg04err = { _id, content: { data: `${t('apiErr')}: ${valueObject.message}` }, type: cardType };
updateTypeMsg(_msg04err);
updateHistoryMsg(OpenedId, _msg04err);
}
else if (eventName) {
console.log('其他类型的信息:', eventName);
if (!retStr.trim()) {
}
}
});
},
onCatch(err) {
console.log('reqQAChatFetch onCatch', err);
if (err === 'BodyStreamBuffer was aborted') {
retStr += `-- ${t('interrupt')} --`;
}
else {
retStr += err || t('reqExce');
}
},
onFinally() {
const _msgend = { _id, content: { data: retStr }, type: cardType };
updateTypeMsg(_msgend);
updateHistoryMsg(OpenedId, _msgend);
updateHistoryComposer(OpenedId, { controller: null });
},
});
}
}
function userSendMsg(inVal) {
const _msg = { _id: RandomString(), type: 'Text', content: { data: inVal } };
const _d_msg = MyBeforeSendMessage(_msg, GlobalCtxs);
if (_d_msg === false)
return;
setText('');
setMyPattern('stop');
appendMsgFn('right', _d_msg, true);
const { content } = _d_msg;
const _nextOrVal = MySendMessage(content?.data, GlobalCtxs);
if (!_nextOrVal)
return;
const load_id = RandomString();
const n_msg = { _id: load_id, type: 'Load', content: {} };
appendMsgFn('left', n_msg, true);
dealRequest_FETCH(load_id, _nextOrVal);
}
function dealDefaultMsg() {
return (isArray(defaultMessages) ? defaultMessages : []).map(value => {
if (value.position === 'center' || value.type === 'System')
return value;
return extend({}, { user: value.position === 'left' ? myRobot : myUser }, value);
});
}
function sendAfterOpenChat() {
typeof onAfterOpenChat === 'function' && onAfterOpenChat({ data: { openedId: myTopicOpened }, ctx: GlobalCtxs });
}
function getLoadContent(content, iconClass, contentClass) {
return _jsxs("div", { className: 'DS-GA-flex DS-GA-gap-1', children: [_jsx(LuLoader, { className: classNames('DS-GA-text-base', iconClass) }), _jsx("span", { className: classNames(contentClass), children: content })] });
}
function requestHistoryTo(_place) {
return new Promise((resolve, reject) => {
if (typeof reqHistoryMsg === 'function') {
const _preData = {
topicInfo: topic.find(myTopicOpened),
};
const _reqConfig = reqHistoryMsg({ data: _preData, ctx: GlobalCtxs });
sendRequestWithConfig({
commonOpt: { timeout, method, headers },
parameter: _reqConfig,
isFecth: false,
controller: null,
onResponse(list) {
console.log('reqHistoryMsg onResponse', list);
if (Array.isArray(list)) {
if (_place === 'up')
list.filter(item => appendMsgFn(item.position, item, false, 'up'));
if (_place === 'down')
list.filter(item => appendMsgFn(item.position, item, false, 'down'));
}
resolve({ success: true });
},
onCatch(err) {
console.log('reqHistoryMsg onCatch', err);
resolve({ success: false });
},
onFinally() {
console.log('reqHistoryMsg onFinally');
sendAfterOpenChat();
},
});
}
else {
sendAfterOpenChat();
resolve({ success: true });
}
});
}
function dealLoadResult(fn, ev, ctx, direct) {
fn(ev, ctx, direct).then((opt) => {
const { success, content } = opt;
if (success === true) {
setScrollLoading({ status: 'succeed', direct: direct });
}
else {
setScrollLoading({ status: 'failed', direct: direct });
}
}).catch((err) => {
setScrollLoading({ status: 'failed', direct: direct });
});
}
function onBoxScroll(ev) {
if (GlobalCtxs === undefined)
return;
if (scrollLoading.status === 'pending')
return;
if (!hasMoreUp && !hasMoreDown)
return;
const { offsetHeight, scrollTop, scrollHeight } = ev.target;
const _diffArea = 20;
const _virtualTop = (scrollTop - _diffArea);
const _virtualBottom = (scrollHeight - offsetHeight - scrollTop - _diffArea);
if (lastScrollTop > scrollTop) {
if (_virtualTop <= 0 && hasMoreUp) {
const _dirUp = 'up';
deleteMsg('123');
setScrollLoading({ status: 'pending', direct: _dirUp });
prependMsgs([{ _id: '123', type: 'System', content: { data: getLoadContent(t('loading'), 'is-spin') } }]);
dealLoadResult(MyOnScrollTo, ev, GlobalCtxs, _dirUp);
}
}
else {
if (_virtualBottom <= 0 && hasMoreDown) {
const _dirDown = 'down';
deleteMsg('123');
setScrollLoading({ status: 'pending', direct: _dirDown, });
appendMsg({ _id: '123', type: 'System', content: { data: getLoadContent(t('loading'), 'is-spin') } });
dealLoadResult(MyOnScrollTo, ev, GlobalCtxs, _dirDown);
}
}
lastScrollTop = scrollTop;
}
useEffect(() => {
if (myTopicOpened === null && GlobalCtxs.status === 'succeed') {
if (typeof onBeforeOpenChat === 'function' && isOpenFirst) {
const _firstID = myHistoryList[0]?.id || undefined;
if (_firstID === undefined)
return;
const _boo = onBeforeOpenChat({ data: { currentId: '', afterId: _firstID }, ctx: GlobalCtxs });
if (_boo)
topic.open(_firstID);
}
}
}, [myTopicOpened, myHistoryList, GlobalCtxs.status]);
function updateInputControl(_openid) {
if (historyComposer && historyComposer[_openid] !== undefined) {
const { controller } = historyComposer[_openid];
if (controller === null) {
myController = null;
setMyPattern('send');
return true;
}
else {
myController = controller;
setMyPattern('stop');
return false;
}
}
else {
myController = null;
setMyPattern('send');
return true;
}
}
function updateTopicStatus(_openid, _notReset) {
const _flag = updateInputControl(_openid);
const _findHis = findHistoryMsg(_openid);
console.log('查找当前会话的历史缓存?', _openid, _findHis);
if (_findHis !== undefined && Array.isArray(_findHis)) {
const _List = _findHis[1] || [];
if (!_notReset)
resetList(_List);
if (_flag && _List.length >= 0) {
const _last = _List[_List.length - 1];
if (_last && _notReset) {
const { _id, content, isTyping: _lastIsTyping } = _last;
if (_lastIsTyping || myController !== null) {
updateTypeMsg({ _id, content });
setTimeout(() => { updateTopicStatus(_openid, true); }, 100);
}
}
}
scrollToEnd();
}
}
useEffect(() => {
updateInputControl(myTopicOpened);
}, [myTopicOpened, historyComposer]);
useEffect(() => {
if (myTopicOpened === null || GlobalCtxs?.status !== 'succeed')
return;
GlobalCtxs.source.clearList();
GlobalCtxs.chat.inputRef.setText('');
autoResetMessages && resetList([]);
updateTopicStatus(myTopicOpened);
lastScrollTop = 0;
setHasMoreUp(true);
setHasMoreDown(false);
const _dirDown = 'down';
deleteMsg('__loadMore__');
setScrollLoading({ status: 'pending', direct: _dirDown });
prependMsgs([{ _id: '__loadMore__', type: 'System', content: { data: getLoadContent(t('loading'), 'is-spin') } }]);
dealLoadResult(requestHistoryTo, '', GlobalCtxs, _dirDown);
}, [myTopicOpened, GlobalCtxs.status]);
useEffect(() => {
const MsgCtx = {
getChatMessages,
appendMsg: (posi, msg, scrollToEnd) => appendMsgFn(posi, msg, scrollToEnd, 'down'),
prependMsg: (posi, msg, scrollToTop) => appendMsgFn(posi, msg, scrollToTop, 'up'),
updateMsg: (id, msg) => {
if (!id)
return;
const _oldmsg = getChatMessages().find((_e) => _e._id === id);
const t_msg = defaultsDeep(msg, _oldmsg);
updateMsg(id, t_msg);
},
deleteMsg,
resetList: (_list) => {
let _nlist = _list.map(_item => {
const { _id, position, type, content, user } = _item || {};
if (!type || !content)
return null;
const t_id = _id !== undefined ? _id : RandomString();
if (type === 'System')
return Object.assign({}, _item, { _id: t_id });
const t_posi = position || (position === 'left' ? 'left' : 'right');
return Object.assign({}, _item, { _id: t_id, position: t_posi, user: user || (position === 'left' ? myRobot : myUser) });
});
_nlist = _nlist.filter(_e => _e !== null);
resetList(_nlist);
},
scrollToEnd,
inputRef: _myComposerRef.current?.ele,
breakRequest,
setInputPattern: setMyPattern,
setHistoryHasMoreUp: setHasMoreUp,
setHistoryHasMoreDown: setHasMoreDown,
};
updateChatCtxs(MsgCtx);
}, [_myComposerRef.current]);
useEffect(() => {
setChatMessages(messages);
}, [messages]);
return (_jsxs("div", { className: classNames('DS-GA-grow DS-GA-w-full DS-GA-h-full', 'DS-GA-relative media-pad', (!showAreaLeft && showAreaRight) ? 'right-show-pad' : 'right-hidden-pad'), children: [_jsx(Chat, { messages: messages, ref: _myChatContainer, messagesRef: _myMsgContainer, composerRef: _myComposerRef, inputOptions: {
pattern: myPattern,
}, Composer: myComposerEType, onSend: (type, value) => {
setHasMoreDown(false);
if (type === 'stop') {
breakRequest();
setText('');
return;
}
userSendMsg(value);
}, renderMessageContent: (option) => {
if (typeof renderMessageContent === 'function')
return renderMessageContent({ data: option, ctx: GlobalCtxs });
const isLastMsg = myPattern !== 'stop' && option._id === (messages[messages.length - 1] && messages[messages.length - 1]._id);
return (_jsxs(_Fragment, { children: [option.type === 'System' && (_jsx(TypeCard, { option: { _id: option._id, type: option.type, content: option.content } }, option._id)), option.type !== 'System' && (_jsx(MessageCard, { option: option, refs: { inputRef: _myComposerRef }, isLastMsg: isLastMsg, children: _jsx(TypeCard, { option: option }, option._id) }, option._id))] }));
}, onScroll: onScrollTo && (hasMoreDown || hasMoreUp) ? debounce(onBoxScroll, 300) : undefined }), loadAreaRight && showAreaRight && _jsx(AreaRight, {})] }));
}