vansin
feat: update
dc9e27a
raw
history blame
19.9 kB
import styles from './index.module.less';
import { useEffect, useState, useRef, useMemo, useContext } from 'react';
import { Input, message, Tooltip } from 'antd';
import ShowRightIcon from './assets/think-progress-icon.svg';
import { MindsearchContext } from './provider/context';
import ChatRight from './components/chat-right';
import { useNavigate, useParams } from 'react-router-dom';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import SessionItem from './components/session-item';
import classNames from 'classnames';
import Notice from './components/notice';
interface INodeInfo {
isEnd?: boolean; // 该节点是否结束
current_node?: string;
thinkingData?: string; // step1 思考
queries?: [];
readingData?: string; // step2 思考
searchList?: [];
conclusion?: string; // 节点的结论
selectedIds?: number[];
subQuestion?: string; // 节点的标题
conclusionRef: any[];
outputing?: boolean;
};
interface IFormattedData {
question?: string;
nodes?: any;
adjacency_list?: object;
response?: string;
responseRefList?: any[];
chatIsOver?: boolean;
};
interface INodeItem {
id: string;
name: string;
state: number;
};
class FatalError extends Error { };
class RetriableError extends Error { };
const MindSearchCon = () => {
const navigate = useNavigate();
const params = useParams<{ id: string; robotId: string }>();
const [qaList, setQaList] = useState<IFormattedData[]>([]);
const [formatted, setFormatted] = useState<IFormattedData>({});
const [question, setQuestion] = useState('');
const [stashedQuestion, setStashedQuestion] = useState<string>('');
const [newChatTip, setNewChatTip] = useState<Boolean>(false);
const [singleObj, setSingleObj] = useState<any>(null);
const [isEnd, setIsEnd] = useState(false);
const [inputFocused, setFocused] = useState(false);
// 一轮完整对话结束
const [chatIsOver, setChatIsOver] = useState(true);
const [currentNodeInfo, setCurrentNode] = useState<any>(null);
const [currentNodeName, setCurrentNodeName] = useState<string>('');
const [activeNode, setActiveNode] = useState<string>('');
// 是否展示右侧内容
const [showRight, setShowRight] = useState(false);
const [adjList, setAdjList] = useState<any>({});
const [historyNode, setHistoryNode] = useState<any>(null);
const [hasNewChat, setHasNewChat] = useState(false);
// 新开会话
const openNewChat = () => {
location.reload();
};
const toggleRight = () => {
setShowRight(!showRight);
};
// 渲染过程中保持渲染文字可见
const keepScrollTop = () => {
const divA = document.getElementById('chatArea') as HTMLDivElement;
const divB = document.getElementById('messageWindowId') as HTMLDivElement;
// 获取 divB 的当前高度
const bHeight = divB.offsetHeight;
// 检查 divA 是否需要滚动(即 divB 的高度是否大于 divA 的可视高度)
if (bHeight > divA.offsetHeight) {
// 滚动到 divB 的底部在 divA 的可视区域内
divA.scrollTop = bHeight - divA.offsetHeight + 30;
}
};
const initPageState = () => {
setSingleObj(null);
setCurrentNodeName('');
setCurrentNode(null);
setFormatted({});
setAdjList({});
setShowRight(false);
setIsEnd(false);
};
const responseTimer: any = useRef(null);
useEffect(() => {
// console.log('[ms]---', formatted, chatIsOver, responseTimer.current);
if (chatIsOver && formatted?.response) {
// 一轮对话结束
setQaList((pre) => {
return pre.concat(formatted);
});
initPageState();
setCurrentNodeName('customer-0');
}
if (!chatIsOver && !responseTimer.current) {
responseTimer.current = setInterval(() => {
keepScrollTop();
}, 50);
}
if (responseTimer.current && chatIsOver) {
// 如果 isEnd 变为 false,清除定时器
clearInterval(responseTimer.current);
responseTimer.current = null;
}
}, [formatted?.response, chatIsOver, responseTimer.current, newChatTip]);
useEffect(() => {
if (formatted?.question) {
setHistoryNode(null);
setChatIsOver(false);
}
}, [formatted?.question]);
// 存储节点信息
const stashNodeInfo = (fullInfo: any, nodeName: string) => {
// console.log('stash node info------', fullInfo, fullInfo?.response?.stream_state);
const content = JSON.parse(fullInfo?.response?.content || '{}') || {};
const searchListStashed: any = Object.keys(content).map((item) => {
return { id: item, ...content[item] };
});
const stashedList = JSON.parse(localStorage?.stashedNodes || '{}');
const nodeInfo = stashedList[nodeName] || {};
if (fullInfo?.content) {
nodeInfo.subQuestion = fullInfo.content;
}
if (fullInfo?.response?.formatted?.thought) {
// step1 思考
if (!nodeInfo?.readingData && !nodeInfo?.queries?.length) {
nodeInfo.thinkingData = fullInfo?.response?.formatted?.thought;
}
// step2 思考
if (nodeInfo?.thinkingData && nodeInfo?.queries?.length && nodeInfo?.searchList?.length && !nodeInfo?.selectedIds?.length && !nodeInfo?.conclusion) {
nodeInfo.readingData = fullInfo?.response?.formatted?.thought;
}
// conclusion
if (nodeInfo?.startConclusion && fullInfo?.response?.stream_state === 1) {
nodeInfo.conclusion = fullInfo?.response?.formatted?.thought;
}
}
if (fullInfo?.response?.formatted?.action?.parameters?.query?.length && !nodeInfo.queries?.length) {
nodeInfo.queries = fullInfo?.response?.formatted.action.parameters.query;
}
if (searchListStashed?.length && !nodeInfo.conclusionRef) {
nodeInfo.searchList = searchListStashed;
nodeInfo.conclusionRef = content;
}
if (Array.isArray(fullInfo?.response?.formatted?.action?.parameters?.select_ids) && !nodeInfo?.selectedIds?.length) {
nodeInfo.selectedIds = fullInfo?.response?.formatted.action.parameters.select_ids;
nodeInfo.startConclusion = true;
}
if (fullInfo?.response?.stream_state) {
nodeInfo.outputing = true;
} else {
nodeInfo.outputing = false;
}
const nodesList: any = {};
nodesList[nodeName] = {
current_node: nodeName,
...nodeInfo,
};
window.localStorage.stashedNodes = JSON.stringify({ ...stashedList, ...nodesList });
};
const formatData = (obj: any) => {
// 嫦娥6号上有哪些国际科学载荷?它们的作用分别是什么?
try {
// 更新邻接表
if (obj?.response?.formatted?.adjacency_list) {
setAdjList(obj.response?.formatted?.adjacency_list);
}
if (!obj?.current_node && obj?.response?.formatted?.thought && obj?.response?.stream_state === 1) {
// 有thought,没有node, planner思考过程
setFormatted((pre: IFormattedData) => {
return {
...pre,
response: obj.response.formatted.thought,
};
});
}
if (obj?.response?.formatted?.ref2url && !formatted?.responseRefList) {
setFormatted((pre: IFormattedData) => {
return {
...pre,
responseRefList: obj?.response?.formatted?.ref2url,
};
});
}
if (obj?.current_node || obj?.response?.formatted?.node) {
// 有node, 临时存储node信息
stashNodeInfo(obj?.response?.formatted?.node?.[obj.current_node], obj.current_node);
}
} catch (err) {
console.log(err);
}
};
const handleError = (errCode: number, msg: string) => {
message.warning(msg || '请求出错了,请稍后再试');
if (errCode === -20032 || errCode === -20033 || errCode === -20039) {
// 敏感词校验失败, 新开会话
openNewChat();
return;
}
console.log('handle error------', msg);
setChatIsOver(true);
initPageState();
};
const startEventSource = () => {
console.log('start event--------');
if (qaList?.length > 4) {
setNewChatTip(true);
message.warning('对话数已达上限,请在新对话中聊天');
keepScrollTop();
return;
}
setFormatted({ ...formatted, question });
setQuestion('');
setChatIsOver(false);
const ctrl = new AbortController();
const url = '/solve';
// const queryData = {
// cancel: true,
// prompt: question,
// };
const postData = {
inputs: question
}
fetchEventSource(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
openWhenHidden: true,
signal: ctrl.signal,
onmessage(ev) {
try {
const res = (ev?.data && JSON.parse(ev.data)) || null;
if (res?.response?.stream_state === 0) {
setChatIsOver(true);
setFormatted((pre: IFormattedData) => {
return {
...pre,
chatIsOver: true,
};
});
} else {
formatData(res);
setSingleObj(res);
}
} catch (err) {
console.log('error on sse---', err);
handleError(0, '请求出错了,请稍后再试!');
}
},
onerror(err) {
console.log('error on sse---', err);
handleError(0, '');
ctrl.abort();
if (err instanceof FatalError) {
throw err;
}
},
onclose() {
// params?.id && handleUpdateHistoryItem(params?.id);
}
});
};
// 点击节点
const handleNodeClick = (node: string, idx: number) => {
if (isEnd && !chatIsOver) return; // 当节点输出完成,最终response进行中,不允许点击按钮,点击无效
const isFromHistory = qaList?.[idx]?.nodes?.[node];
setShowRight(true);
setActiveNode(node);
if (isFromHistory) {
const info = qaList?.[idx]?.nodes?.[node];
if (!info) {
message.error('没有读取到节点信息');
}
setHistoryNode(info);
} else {
setCurrentNodeName(node);
}
};
// 解析历史记录或者搜索返回的数据
const formatHistoryNode = (originNodeInfo: any) => {
// console.log('format history node--------', originNodeInfo);
const searchContent = JSON.parse(originNodeInfo?.memory?.[1]?.content || '{}') || {};
const searchListStashed: any = Object.keys(searchContent).map((item) => {
return { id: item, ...searchContent[item] };
});
const nodeInfo: INodeInfo = {
current_node: originNodeInfo?.current_node || String(Date.now()),
thinkingData: originNodeInfo?.memory?.[0]?.formatted?.thought || '', // step1 思考
queries: originNodeInfo?.memory?.[0]?.formatted?.action?.parameters?.query || [],
readingData: originNodeInfo?.memory?.[2]?.formatted?.thought || '', // step2 思考
searchList: searchListStashed,
conclusionRef: searchContent,
conclusion: originNodeInfo?.memory?.[4]?.formatted?.thought || '', // 节点的结论
selectedIds: originNodeInfo?.memory?.[2]?.formatted?.action?.parameters?.select_ids || [],
subQuestion: originNodeInfo?.content, // 节点的标题
isEnd: true,
outputing: false
};
return nodeInfo;
};
const createSseChat = () => {
if (submitDisabled) {
return;
}
setQuestion(stashedQuestion);
setStashedQuestion('');
setCurrentNodeName('customer-0');
};
const checkNodesOutputFinish = () => {
const adjListStr = JSON.stringify(adjList);
// 服务端没有能准确描述所有节点输出完成的状态,前端从邻接表信息中寻找response信息,不保证完全准确,因为也可能不返回
if (adjListStr.includes('"name":"response"')) {
setIsEnd(true);
}
};
useEffect(() => {
if (!adjList) return;
if (isEnd) {
// 所有节点输出完成时收起右侧
setShowRight(false);
} else {
checkNodesOutputFinish();
}
setFormatted((pre: IFormattedData) => {
return {
...pre,
adjacency_list: adjList,
};
});
}, [adjList, isEnd]);
useEffect(() => {
const findStashNode = localStorage?.stashedNodes && JSON.parse(localStorage?.stashedNodes || '{}');
if (!findStashNode || !currentNodeName) return;
currentNodeName === 'customer-0' ? setCurrentNode(null) : setCurrentNode(findStashNode?.[currentNodeName]);
currentNodeName !== 'customer-0' && setShowRight(true);
}, [currentNodeName, localStorage?.stashedNodes]);
useEffect(() => {
if (!singleObj) return;
if ((!currentNodeName || currentNodeName === 'customer-0') && singleObj?.current_node) {
setCurrentNodeName(singleObj?.current_node);
}
}, [singleObj, currentNodeName]);
useEffect(() => {
if (question) {
startEventSource();
}
}, [question]);
useEffect(() => {
if (!showRight) {
setActiveNode('');
}
}, [showRight]);
useEffect(() => {
localStorage.stashedNodes = '';
localStorage.reformatStashedNodes = '';
return () => {
// 返回清理函数,确保组件卸载时清除定时器
if (responseTimer.current) {
clearInterval(responseTimer.current);
responseTimer.current = null;
}
};
}, []);
const submitDisabled = useMemo(() => {
return newChatTip || !stashedQuestion || !chatIsOver;
}, [newChatTip, stashedQuestion, chatIsOver]);
return (
<MindsearchContext.Provider value={{
isEnd,
chatIsOver,
activeNode: activeNode
}}>
<div className={styles.mainPage} style={!showRight ? { maxWidth: '840px' } : {}}>
<div className={styles.chatContent}>
<div className={classNames(
styles.top,
(isEnd && !chatIsOver) ? styles.mb12 : ''
)} id="chatArea">
<div id="messageWindowId">
{qaList.length > 0 &&
qaList.map((item: IFormattedData, idx) => {
return (
<div key={`qa-item-${idx}`} className={styles.qaItem}>
{
item.question && <SessionItem
item={item}
handleNodeClick={handleNodeClick}
idx={idx}
key={`session-item-${idx}`}
/>
}
</div>
);
})
}
{
formatted?.question &&
<SessionItem
item={{ ...formatted, chatIsOver, isEnd, adjacency_list: adjList }}
handleNodeClick={handleNodeClick}
idx={qaList.length}
/>
}
</div>
{newChatTip && (
<div className={styles.newChatTip}>
<span>
对话数已达上限,请在 <a onClick={openNewChat}>新对话</a> 中聊天
</span>
</div>
)}
</div>
<div className={classNames(
styles.input,
inputFocused ? styles.focus : ''
)}>
<div className={styles.inputMain}>
<div className={styles.inputMainBox}>
<Input
className={styles.textarea}
variant="borderless"
value={stashedQuestion}
placeholder={'开始提问吧'}
onChange={(e) => {
setStashedQuestion(e.target.value);
}}
onPressEnter={createSseChat}
onFocus={() => { setFocused(true) }}
onBlur={() => { setFocused(false) }}
/>
<div className={classNames(styles.send, submitDisabled && styles.disabled)} onClick={createSseChat}>
<i className="iconfont icon-Frame1" />
</div>
</div>
</div>
</div>
<Notice />
</div>
{showRight ? (
<ChatRight
nodeInfo={currentNodeInfo}
historyNode={historyNode}
toggleRight={toggleRight}
key={currentNodeName}
chatIsOver={chatIsOver}
/>
) : (
<div className={styles.showRight}>
<div className={classNames(
styles.actionIcon,
isEnd && !chatIsOver ? styles.forbidden : ''
)} onClick={toggleRight}>
<Tooltip placement="leftTop" title="思考过程">
<img src={ShowRightIcon} />
</Tooltip>
</div>
</div>
)}
</div>
</MindsearchContext.Provider>
);
};
export default MindSearchCon;