UNPKG

general-assistant

Version:

General Assistant components

583 lines (582 loc) 27.1 kB
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, {})] })); }