vansin's picture
feat: update
f3f614f
raw
history blame
24.7 kB
import styles from './index.module.less';
import { useEffect, useState, useRef, Children } from 'react';
import MindMapItem from './mindMapItem';
import PackIcon from '@/assets/pack-up.svg';
import SendIcon from '@/assets/sendIcon.svg';
import { Tooltip, Input, message } from 'antd';
import IconFont from '@/components/iconfont';
import ReactMarkdown from "react-markdown";
import ShowRightIcon from "@/assets/show-right-icon.png";
import rehypeRaw from 'rehype-raw';
import classNames from 'classnames';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { GET_SSE_DATA } from '@/config/cgi';
import { replaceStr } from '@/utils/tools';
const RenderTest = () => {
let eventSource: any = null;
let sseTimer: any = useRef(null);
const [isWaiting, setIsWaiting] = useState(false);
const [question, setQuestion] = useState("");
const [stashQuestion, setStashQuestion] = useState("");
const [isEnd, setIsEnd] = useState(false);
const [showEndNode, setShowEndNode] = useState(false);
// 一组节点的渲染草稿
const [draft, setDraft] = useState('');
// 一轮完整对话结束
const [chatIsOver, setChatIsOver] = useState(true);
// 一组节点的思考草稿是不是打印结束
const [draftEnd, setDraftEnd] = useState(false);
const [progress1, setProgress1] = useState('');
const [progress2, setProgress2] = useState('');
const [progressEnd, setProgressEnd] = useState(false);
const [conclusion, setConclusion] = useState('');
const [stashConclusion, setstashConclusion] = useState('');
const [query, setQuery] = useState([]);
const [searchList, setSearchList] = useState([]);
// 整体的渲染树
const [renderData, setRenderData] = useState<any[]>([]);
const [currentNode, setCurrentNode] = useState<any>(null);
// 渲染minddata里的第几个item
const [renderIndex, setRenderIndex] = useState<number>(0);
const [response, setResponse] = useState("");
const [currentStep, setCurrentStep] = useState(0);
// steps展开收起的信息
const [collapseInfo, setCollapseInfo] = useState([true, true]);
const [mapWidth, setMapWidth] = useState(0);
// 是否展示右侧内容
const [showRight, setShowRight] = useState(true);
const [currentNodeRendering, setCurrentNodeRendering] = useState(false);
const [selectedIds, setSelectedIds] = useState([]);
const [nodeName, setNodeName] = useState('');
const hasHighlight = useRef(false);
const conclusionRender = useRef(false);
const nodeDraftRender = useRef(false);
const [obj, setObj] = useState<any>(null);
const [nodeOutputEnd, setNodeEnd] = useState(false);
const [adjList, setAdjList] = useState([]);
const TEXT_INTERVAL = 20;
const SEARCHLIST_INTERVAL = 80;
const toggleRight = () => {
setShowRight(!showRight);
};
const findAndUpdateStatus = (nodes: any[], targetNode: any) => {
return nodes.map((node) => {
if (node.state === 1 && node.id !== 0) {
return { ...node, state: 3 };
}
if (node.name === targetNode) {
return { ...node, state: 1 };
}
if (node.children) {
// 递归地在子节点中查找
node.children = findAndUpdateStatus(node.children, targetNode);
}
return node;
});
}
const generateEndStyle = () => {
// 获取所有class为endline的div元素
const endlineDivs = document.getElementsByClassName('endline');
const mindMap = document.getElementById("mindMap");
// 确保至少有两个元素
if (endlineDivs.length >= 2 && mindMap) {
// 获取第一个和最后一个元素的边界框(bounding rectangle)
const firstRect = endlineDivs[0].getBoundingClientRect();
const lastRect = endlineDivs[endlineDivs.length - 1].getBoundingClientRect();
const mindMapRect = mindMap?.getBoundingClientRect();
// 计算y值的差值
const yDiff = lastRect.top - firstRect.top;
// const top = firstRect.top - mindMapRect.top;
// 如果需要包含元素的完整高度(不仅仅是顶部位置),可以加上元素的高度
// const yDiffWithHeight = yDiff + (lastRect.height - firstRect.height);
return {
top: firstRect.top - mindMapRect.top,
height: yDiff + 1
};
} else {
return {
top: '50%',
height: 0
};
}
};
const generateWidth = () => {
const articles = document.querySelectorAll('article');
// 确保至少有两个元素
if (articles?.length) {
let maxRight = 0;
articles.forEach((item, index) => {
if (item.getBoundingClientRect().right > maxRight) {
maxRight = item.getBoundingClientRect().right;
}
})
const firstArticle = articles[0].getBoundingClientRect();
if (maxRight - firstArticle.left + 200 > mapWidth) {
return maxRight - firstArticle.left + 200
} else {
return mapWidth;
}
} else {
return 100;
}
};
// 逐字渲染
const renderDraft = (str: string, type: string, endCallback: () => void) => {
// 已经输出的字符数量
let outputIndex = 0;
// 输出字符的函数
const outputText = () => {
// 给出高亮后draft输出的结束标志
if (type === 'stepDraft-1' && outputIndex + 3 > str?.length) {
nodeDraftRender.current = true;
}
// 如果还有字符未输出
if (outputIndex < str?.length) {
// 获取接下来要输出的1个字符(或剩余字符,如果不足3个)
let chunk = str.slice(outputIndex, Math.min(outputIndex + 10, str.length));
// 更新已输出字符的索引
outputIndex += chunk.length;
if (type === 'thought') {
setDraft(str.slice(0, outputIndex));
} else if (type === "stepDraft-0") {
setProgress1(str.slice(0, outputIndex));
} else if (type === "stepDraft-1") {
setProgress2(str.slice(0, outputIndex));
} else if (type === "conclusion") {
setConclusion(str.slice(0, outputIndex));
} else if (type === "response") {
setResponse(str.slice(0, outputIndex));
}
} else {
// 如果没有更多字符需要输出,则清除定时器
clearInterval(intervalId);
endCallback && endCallback()
}
}
// 设定定时器ID
let intervalId = setInterval(outputText, TEXT_INTERVAL);
}
// 渲染搜索结果renderSearchList
const renderSearchList = () => {
let outputIndex = 0;
const content = JSON.parse(currentNode.actions[currentStep].result[0].content);
const arr: any = Object.keys(content).map(item => {
return { id: item, ...content[item] };
});
const len = Object.keys(content).length;
const outputText = () => {
outputIndex++;
if (outputIndex < len + 1) {
setSearchList(arr.slice(0, outputIndex));
} else {
clearInterval(intervalId);
}
};
// 设定定时器ID
let intervalId = setInterval(outputText, SEARCHLIST_INTERVAL);
};
// 高亮searchList
const highLightSearchList = (ids: any) => {
setSelectedIds([]);
const newStep = currentStep + 1;
setCurrentStep(newStep);
const highlightArr: any = [...searchList];
highlightArr.forEach((item: any) => {
if (ids.includes(Number(item.id))) {
item.highLight = true;
}
})
highlightArr.sort((item1: any, item2: any) => {
if (item1.highLight === item2.highLight) {
return 0;
}
// 如果item1是highlight,放在前面
if (item1.highLight) {
return -1;
}
// 如果item2是highlight,放在后面
return 1;
})
setSearchList(highlightArr);
renderDraft(currentNode.actions[1].thought, `stepDraft-1`, () => { });
hasHighlight.current = true; // 标记为高亮已执行
};
// 渲染结论
const renderConclusion = () => {
const res = window.localStorage.getItem('nodeRes') || '';
const replaced = replaceStr(res);
// setTimeout(() => { setCollapseInfo([false, false]); }, 2000);
setCollapseInfo([false, false]);
setConclusion(replaced);
setstashConclusion(res);
// 给出conclusion结束的条件
if (stashConclusion.length + 5 > res.length) {
conclusionRender.current = true;
setProgressEnd(true);
}
};
// 渲染query
const renderQuery = (endCallback: () => void) => {
const queries = currentNode.actions[currentStep]?.args?.query;
setQuery(queries);
endCallback && endCallback();
};
const renderSteps = () => {
setCurrentNodeRendering(true);
const queryEndCallback = () => {
if (currentNode.actions[currentStep].result[0].content) {
if (currentNode.actions[currentStep].type === "BingBrowser.search" || currentNode.actions[currentStep].type === "BingBrowser") {
renderSearchList();
}
}
};
const thoughtEndCallback = () => {
if (currentNode.actions[currentStep]?.args?.query?.length) {
renderQuery(queryEndCallback);
} else {
queryEndCallback();
}
};
if (currentNode.actions[currentStep].thought) {
renderDraft(currentNode.actions[currentStep].thought, `stepDraft-${currentStep}`, thoughtEndCallback);
}
}
// 展开收起
const toggleCard = (index: number) => {
const arr = [...collapseInfo];
arr[index] = !arr[index];
setCollapseInfo(arr);
};
// 渲染过程中保持渲染文字可见
const keepScrollTop = (divA: any, divB: any) => {
// 获取 divB 的当前高度
const bHeight = divB.offsetHeight;
// 检查 divA 是否需要滚动(即 divB 的高度是否大于 divA 的可视高度)
if (bHeight > divA.offsetHeight) {
// 滚动到 divB 的底部在 divA 的可视区域内
divA.scrollTop = bHeight - divA.offsetHeight;
}
};
useEffect(() => {
setRenderData([
{
id: 0,
state: 3,
name: '原始问题',
children: adjList
}
])
}, [JSON.stringify(adjList)]);
useEffect(() => {
console.log('render data changed-----', renderData);
}, [renderData]);
useEffect(() => {
if (currentStep === 1) {
setCollapseInfo([false, true]);
}
}, [currentStep]);
useEffect(() => {
if (nodeOutputEnd && !localStorage.getItem('nodeRes')) {
// 如果节点输出结束了,但是response还没有结束,认为节点渲染已结束
conclusionRender.current = true;
setProgressEnd(true);
return;
}
if (nodeDraftRender.current && localStorage.getItem('nodeRes')) {
renderConclusion();
}
}, [localStorage.getItem('nodeRes'), nodeDraftRender.current, nodeOutputEnd]);
useEffect(() => {
if (obj?.response?.nodes[obj.current_node]?.detail?.state !== 1) {
setIsWaiting(true);
}
if (obj?.response?.nodes?.[obj.current_node].detail?.state === 0 && currentNode?.current_node === obj.current_node) {
console.log('node render end-----', obj);
setNodeEnd(true);
}
if (obj?.current_node && obj?.response?.state === 3) {
// 当node节点的数据可以开始渲染时,给currentnode赋值
// update conclusion
if (obj.response.nodes[obj.current_node]?.detail?.actions?.length === 2 &&
obj.response.nodes[obj.current_node]?.detail?.state === 1 &&
obj.response.nodes[obj.current_node]?.detail.response) {
window.localStorage.setItem('nodeRes', obj.response.nodes[obj.current_node]?.detail.response);
}
if (obj.current_node &&
(obj.response.nodes[obj.current_node]?.detail?.state === 1) &&
obj.response.nodes[obj.current_node]?.detail?.actions?.length &&
currentStep === 0 &&
currentNode?.current_node !== obj?.current_node
) {
// 更新当前渲染节点
console.log('update current node----');
setIsWaiting(false);
setCurrentNode({ ...obj.response.nodes[obj.current_node]?.detail, current_node: obj.current_node });
}
// 设置highlight
if (!selectedIds.length &&
obj.response.nodes[obj.current_node]?.detail?.actions?.[1]?.type === 'BingBrowser.select' &&
(obj.response.nodes[obj.current_node]?.detail?.state === 1)) {
setSelectedIds(obj.response.nodes[obj.current_node]?.detail?.actions?.[1]?.args?.select_ids || []);
setCurrentNode({ ...obj.response.nodes[obj.current_node]?.detail, current_node: obj.current_node });
}
}
}, [obj]);
useEffect(() => {
// 输出思考过程
if (!currentNode || currentNodeRendering) { return; }
renderSteps();
}, [currentNode, currentNodeRendering, selectedIds]);
useEffect(() => {
if (!hasHighlight.current && selectedIds.length && currentNode?.actions.length === 2) {
// 渲染高亮的search信息
highLightSearchList(selectedIds);
}
}, [selectedIds, currentNode]);
useEffect(() => {
// 当前节点渲染结束
if (nodeName && nodeName !== currentNode?.current_node && progressEnd && !isEnd) {
resetNode(nodeName);
setMapWidth(generateWidth());
}
}, [nodeName, currentNode, progressEnd, isEnd]);
let responseTimer: any = useRef(null);
useEffect(() => {
if (isEnd) {
responseTimer.current = setInterval(() => {
const divA = document.getElementById('chatArea') as HTMLDivElement;
const divB = document.getElementById('messageWindowId') as HTMLDivElement;
keepScrollTop(divA, divB);
if (chatIsOver) {
clearInterval(responseTimer.current);
}
}, 500);
setTimeout(() => {
setShowEndNode(true);
}, 300);
} else if (responseTimer.current) {
// 如果 isEnd 变为 false,清除定时器
clearInterval(responseTimer.current);
responseTimer.current = null;
}
// 返回清理函数,确保组件卸载时清除定时器
return () => {
if (responseTimer.current) {
clearInterval(responseTimer.current);
responseTimer.current = null;
}
};
}, [isEnd, chatIsOver]);
useEffect(() => {
setRenderData([]);
setResponse('');
setDraft('');
setIsEnd(false);
setShowRight(true);
window.localStorage.setItem('nodeRes', '');
window.localStorage.setItem('finishedNodes', '');
}, [question]);
const resetNode = (targetNode: string) => {
if (targetNode === 'response') return; // 如果开始response了,所有节点都渲染完了,不需要reset
// 渲染下一个节点前,初始化状态
const newData = findAndUpdateStatus(renderData, targetNode);
console.log('reset node------', targetNode, renderData);
setCurrentStep(0);
setQuery([]);
setSearchList([]);
setConclusion('');
setCollapseInfo([true, true]);
setProgress1('');
setProgress2('');
setProgressEnd(false);
setCurrentNode(null);
setCurrentNodeRendering(false);
setSelectedIds([]);
setNodeEnd(false);
hasHighlight.current = false;
nodeDraftRender.current = false;
conclusionRender.current = false;
window.localStorage.setItem('nodeRes', '');
};
const formatData = (data: any) => {
try {
setIsWaiting(false);
const obj = JSON.parse(data);
if (!obj.current_node && obj.response.state === 0) {
console.log('chat is over end-------');
setChatIsOver(true);
return;
}
if (!obj.current_node && obj.response.state === 9) {
setShowRight(false);
setIsEnd(true);
const replaced = replaceStr(obj.response.response);
setResponse(replaced);
return;
}
if (!obj.current_node && obj.response.state === 1 && !currentNode) {
// 有thought,没有node
setDraftEnd(false);
setDraft(obj.response.response);
}
if (!obj.current_node && (obj.response.state !== 1 || obj.response.state !== 0 || obj.response.state !== 9)) {
// 有thought,没有node, 不用处理渲染
//console.log('loading-------------', obj);
setDraftEnd(true);
setIsWaiting(true);
}
if (obj.current_node && obj.response.state === 3) {
setNodeName(obj.current_node);
// 有node
setObj(obj);
const newAdjList = obj.response?.adjacency_list;
if (newAdjList?.length > 0) {
setAdjList(newAdjList);
}
}
} catch (err) {
console.log('format error-----', err);
}
};
const startEventSource = () => {
if (!chatIsOver) {
message.warning('有对话进行中!');
return;
}
setQuestion(stashQuestion);
setChatIsOver(false);
const postData = {
inputs: [
{
role: 'user',
content: stashQuestion
}
]
}
const ctrl = new AbortController();
eventSource = fetchEventSource(GET_SSE_DATA, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
onmessage(ev) {
formatData(ev.data);
},
onerror(err) {
console.log('sse error------', err);
},
// signal: ctrl.signal,
});
};
const abortEventSource = () => {
if (eventSource) {
eventSource.close(); // 或使用其他方法关闭连接,具体取决于库的API
eventSource = null;
console.log('EventSource connection aborted due to timeout.');
message.error('连接中断,2s后即将刷新页面---');
setTimeout(() => {
location.reload();
}, 2000);
}
};
return <div className={styles.mainPage} style={!showRight ? { maxWidth: '1000px' } : {}}>
<div className={styles.chatContent}>
<div className={styles.top} id="chatArea">
<div id="messageWindowId">
{
question && <div className={styles.question}>
<span>{question}</span>
</div>
}
{
(draft || response || renderData?.length > 0) &&
<div className={styles.answer}>
{
renderData?.length > 0 ? <div className={styles.inner}>
<div className={styles.mapArea}>
<ul className={styles.mindmap} id="mindMap" style={isEnd ? { width: mapWidth, overflow: "hidden" } : {}}>
{renderData.map((item: any) => (
<MindMapItem key={item.name} item={item} isEnd={isEnd} />
))}
{showEndNode &&
<div className={styles.end} style={generateEndStyle()}>
<div className={styles.node}>
<article>最终回复</article>
</div>
</div>
}
</ul>
</div>
</div> : <></>
}
{
!response && <div className={styles.draft}>
{/* {!draftEnd && draft && <div className={styles.loading}>
<div></div>
<div></div>
<div></div>
</div>} */}
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{replaceStr(draft)}</ReactMarkdown>
</div>
}
{response && <div className={styles.response}>
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{response}</ReactMarkdown>
</div>}
</div>
}
</div>
</div>
<div className={styles.sendArea}>
<Input type="text" placeholder='说点什么吧~ Shift+Enter 换行 ; Enter 发送' onChange={(e) => { setStashQuestion(e.target.value) }}
onPressEnter={startEventSource} />
<button onClick={startEventSource}>
<img src={SendIcon} />
发送
</button>
</div>
<div className={styles.notice}>如果想要更丝滑的体验,请在本地搭建-<a href='https://github.com/InternLM/MindSearch' target='_blank'>MindSearch <IconFont type='icon-GithubFilled' /></a></div>
</div>
{showRight && <div className={styles.progressContent}>
{
currentNode && <>
<div className={styles.toggleIcon} onClick={toggleRight}>
<Tooltip placement="top" title="收起">
<img src={PackIcon} />
</Tooltip></div>
<div className={styles.titleNode}>{currentNode?.content || currentNode?.node}</div>
{
currentNode?.actions?.length ? <>
{
currentNode.actions.map((item: any, idx: number) => (
currentStep >= idx && <div className={classNames(
styles.steps,
item.type === "BingBrowser.search" ? styles.thinking : styles.select
)} key={`step-${idx}`}>
<div className={styles.title}>
<i></i>{item.type === "BingBrowser.search" ? "思考" : item.type === "BingBrowser.select" ? "信息来源" : "信息整合"}
<div className={styles.open} onClick={() => { toggleCard(idx) }}>
<IconFont type={collapseInfo[idx] ? "icon-shouqi" : "icon-xiangxiazhankai"} />
</div>
</div>
<div className={classNames(
styles.con,
!collapseInfo[idx] ? styles.collapsed : ""
)}>
{
item.type === "BingBrowser.search" && <div className={styles.thought}>
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{progress1}</ReactMarkdown>
</div>
}
{
item.type === "BingBrowser.search" && query.length > 0 && <div className={styles.query}>
<div className={styles.subTitle}><IconFont type="icon-SearchOutlined" />搜索关键词</div>
{
query.map((item, index) => (<div key={`query-item-${item}`} className={classNames(styles.queryItem, styles.fadeIn)}>
{item}
</div>))
}
</div>
}
{
currentStep === idx && searchList.length > 0 && <div className={styles.searchList}>
{item.type === "BingBrowser.search" && <div className={styles.subTitle}><IconFont type="icon-DocOutlined" />信息来源</div>}
{
item.type === "BingBrowser.select" && <div className={styles.thought}>
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{progress2}</ReactMarkdown>
</div>
}
<div className={styles.scrollCon} style={(searchList.length > 5 && currentStep === 0) ? { height: '300px' } : {}}>
<div className={styles.inner} style={(searchList.length > 5 && currentStep === 0) ? { position: 'absolute', bottom: 0, left: 0 } : {}}>
{
searchList.map((item: any, num: number) => (
<div className={classNames(
styles.searchItem,
item.highLight ? styles.highLight : ""
)} key={`search-item-${item.url}-${idx}`}>
<p className={styles.summ}>{item.id}. {item?.title}</p>
<p className={styles.url}>{item?.url}</p>
</div>
))
}
</div>
</div>
</div>
}
</div>
</div>
))
}
</> : <></>
}
</>
}
{
conclusion && <div className={styles.steps}>
<div className={styles.title}>
<i></i>信息整合
</div>
<div className={styles.conclusion}>
<ReactMarkdown rehypePlugins={[rehypeRaw]}>{conclusion}</ReactMarkdown>
</div>
</div>
}
{isWaiting && question && <div className={styles.loading99}></div>}
</div>}
{
!showRight && <div className={styles.showRight} onClick={toggleRight}>
<img src={ShowRightIcon} />
</div>
}
</div>
};
export default RenderTest;