typescript
复制代码
import React, { useState, useEffect, useRef, useMemo } from 'react';
import './index.less';
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { getAccessToken, getCurrentOrganizationId } from 'hzero-front/lib/utils/utils';
import { getQuestionHistory, getTreeList, getRole, getTreeByOrgName, getTreeByUnitId } from "@/services/api-page";
import chatGPT from '@/assets/images/chatGPT.webp';
import user from '@/assets/images/user.webp';
import { Form, Tooltip, TreeSelect, useDataSet, Icon } from 'choerodon-ui/pro';
import { dataSet, optionDs } from "./store/index";
import { ButtonColor } from 'choerodon-ui/pro/lib/button/enum';
import { Button } from 'choerodon-ui/pro';
import { message } from 'hzero-ui';
import Search from 'choerodon-ui/lib/input/Search';
import { Input } from 'element-react';
import { Select } from 'element-react'
import 'element-theme-default';
const { TreeNode } = TreeSelect;
interface Message {
errorTip: string | undefined;
id: string;
sender: 'user' | 'bot';
text: string | undefined;
files?: any[];
fileName?: string; // 新增用于存储文件名的属性
truncatedContent?: string; // 新增用于存储截断内容的属性
finish?: boolean; // 新增用于存储是否完成的状态
}
const ChatApp: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]); // 渲染的消息列表
const [userInput, setUserInput] = useState('');
const [showButton, setShowButton] = useState(false)
const showData = useDataSet(dataSet, []);
const currentBotMessageRef = useRef<{
truncatedContent: any;
fileName: any; id: string; text: string; files?: any[];
}>({
id: '',
text: '',
files: [], // 初始化为空数组
truncatedContent: '',
fileName: '',
// id: '',
// text: '',
// files?: any[] | undefined,
}); // 当前正在更新的机器人消息
const [process, setProcess] = useState([]);
const controllerRef = useRef(new AbortController());
const [orgId, setOrgId] = useState<string>(); //获取组织id
const [userId, setUserId] = useState('') //当亲登录的用户id
const [selectValue, setSelectValue] = useState() //组织机构树选择的值
const [selectionOptions, setSelectionOptions] = useState([]) //组织机构树的数据
const [isSelectVisible, setIsSelectVisible] = useState(false); //控制组织机构树是否显示
// 通过sse流获取到流数据
const startSseWithPost = async (userMessageId: string) => {
currentBotMessageRef.current = { id: `${userMessageId}-bot`, text: '' }; // 初始化机器人消息
await fetchEventSource(`http://${IP.TEST}${BASIC.CWDMX_MODEL}/${tenantId}/knowledgeBase/getQuestionAnswer`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + getAccessToken(),
},
body: JSON.stringify({
orgId: orgId,
question: userInput,
stream: true,
}),
signal: controllerRef.current.signal,
onmessage(event) {
try {
if (event.data) {
try {
const messageData = JSON.parse(event.data)
const process = messageData?.message?.features?.progress;
setProcess(process);
// if (messageData?.message?.type === 'text') {
const content = messageData?.message?.content;
// 累积回答内容
currentBotMessageRef.current.text += content;
if (messageData?.message?.features && messageData?.message?.features?.doc_citations) {
let fileInfoString = '';
// 遍历 doc_citations 数组
messageData?.message?.features?.doc_citations.forEach(citation => {
citation?.documents && citation?.documents.forEach((item) => {
// setFileName(item.metadata.name)
const fileName = item?.metadata?.name
const truncatedContent = item?.metadata?.doc_ref?.content.length > 14 ? item.metadata?.doc_ref?.content.substring(0, 14) + '...' : item.metadata.doc_ref.content;
const fileObject = {
content: truncatedContent,
url: item?.metadata?.doc_ref?.documentUrl
};
const files: any[] = currentBotMessageRef.current.files || [];
files.push(fileObject)
currentBotMessageRef.current.fileName = fileName;
currentBotMessageRef.current.truncatedContent = truncatedContent;
currentBotMessageRef?.current && (currentBotMessageRef.current.files = files)
});
})
// 动态更新消息状态
setMessages((prevMessages) =>
prevMessages.map((msg) =>
msg.id === currentBotMessageRef.current.id
? {
...msg,
text: `${currentBotMessageRef.current.text}\n${fileInfoString}`,
fileName: currentBotMessageRef.current.fileName,
truncatedContent: currentBotMessageRef.current.truncatedContent,
files: currentBotMessageRef.current.files,
finish: messageData?.finish, // 设置完成状态
errorTip: messageData?.error
}
: msg
)
);
} else {
// 动态更新消息状态,如果没有文件相关信息,只更新文本内容
setMessages((prevMessages) =>
prevMessages.map((msg) =>
msg.id === currentBotMessageRef.current.id
? {
...msg, text: currentBotMessageRef.current.text,
finish: messageData?.finish,
errorTip: messageData?.error
}
: msg
)
);
}
// }
} catch (err) {
console.error('Error parsing message data:', err);
}
}
} catch (error) {
message.error('获取失败');
}
},
onerror(err) {
console.error('SSE error:', err);
throw (err)
},
onclose() {
setShowButton(false)
controllerRef.current.abort();
},
});
};
// 发送消息事件,包括发送的用户输入的消息
const handleSendMessage = () => {
setShowButton(true)
if (userInput.trim()) {
const userMessageId = `${Date.now()}`; // 唯一标识符
// 插入用户消息和占位的机器人消息
setMessages((prevMessages) => [
...prevMessages,
{ id: userMessageId, sender: 'user', text: userInput },
{ id: `${userMessageId}-bot`, sender: 'bot', text: '' }, // 机器人占位消息
]);
startSseWithPost(userMessageId); // 启动 SSE
setUserInput(''); // 清空用户输入
}
};
//停止发送消息
const handleStopFetch = () => {
if (controllerRef.current) {
controllerRef.current.abort();
}
}
// 这里面可以控制node结点的判断来实现是否展示为叶结点
const nodeCover = ({ record }) => {
const nodeProps = {
title: record.get('unitName'),
};
if (record.get('power') === 'false') {
nodeProps.disabled = true;
}
return nodeProps;
}
// 获取选中的组织机构id,需要给问答的接口传参
const handleChange = (val) => {
setOrgId(val)
}
//获取当前登录的用户,并设置userId
const [displayFlag, setdisplayFlag] = useState('false') //是否展示单位
useEffect(() => {
console.log('logfangwende第一次');
const user = async () => {
const res = await getRole();
if (res) {
setUserId(res?.user?.id)
if (userId) {
const res = await getTreeList({ guestId: userId, displayFlag: 'true' });
setdisplayFlag(res?.data?.displayFlag)
}
}
}
user()
}, [userId])
useEffect(() => {
console.log('logfangwende第二次');
if (displayFlag === 'true') {
optionDs.setQueryParameter("guestId", userId);
optionDs.query()
}
}, [displayFlag])
// 获取组织树
useEffect(() => {
console.log('logfangwende第三次');
const chatBox = document.querySelector('.chat-box');
if (chatBox) {
chatBox.scrollTop = chatBox.scrollHeight;
}
}, [messages]);
// 获取问答页历史消息
useEffect(() => {
console.log('logfangwende第四次');
const fetchData = async () => {
try {
const res = await getQuestionHistory();
res.data.forEach((item) => {
if (item?.direction === 1) {
setMessages((prevMessages) => [
...prevMessages,
{ id: item?.mesTime, sender: 'user', text: item?.content, },
// { id: `${item?.mesTime}-bot`, sender: 'bot', text: '' }, // 机器人占位消息
]);
} else if (item?.direction === 2) {
const fileName = item?.ref?.[0]?.title;
const files: any[] = []
item?.ref?.forEach((item) => {
if (item?.content) {
if (item?.content.length > 14) {
const fileObject = {
content: item?.content.substring(0, 14) + '...',
url: item?.docUrl
};
files.push(fileObject);
}
}
})
setMessages((prevMessages) => [
...prevMessages,
{ id: `${item?.mesTime}-bot`, sender: 'bot', text: item?.content, fileName: fileName, files: files, finish: true }, // 机器人占位消息
]);
}
})
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []);
// 处理选中的组织机构
const selectChange = (val) => {
if (!val) return
setIsSelectVisible(false)
setSelectValue(val)
showData.loadData([{ unitName: val, unitCode: val }])
}
// 输入组织机构获取到的数据
const handleEnter = async (value) => {
setSelectionOptions([])
if (value !== '') {
const params = { guestId: userId, unitName: value }
const result = await getTreeByOrgName(params)
if (Array.isArray(result.data)) {
const newArray = result.data.map(item => {
return {
value: item.unitCode,
label: item.unitName
}
})
setSelectionOptions(newArray)
}
}
}
//控制搜索摁扭展示
const handleIconClick = () => {
setSelectValue(undefined)
setIsSelectVisible(true);
setSelectionOptions([])
};
//控制树形组件和下拉选择组件的切换
const handleVisibleChange = (isVisible) => {
if (!isVisible) {
setIsSelectVisible(false);
}
};
// //自动聚焦操作(没实现
// const selectRef = useRef(null);
// useEffect(() => {
// if (selectRef.current) {
// selectRef.current?.focus();
// }
// }, []);
console.log(displayFlag, 'displayFlag');
return (
<div className="chat-container">
<div className="+">
{
displayFlag === 'true' && <div className='search-box'>
{isSelectVisible ? (
<Select
// ref={selectRef}
filterable={true}
remote={true}
remoteMethod={(value) => { handleEnter(value) }}
value={selectValue}
onChange={selectChange}
clearable={true}
placeholder="请输入单位名称"
onVisibleChange={handleVisibleChange}
>
{selectionOptions.map((el, index) => {
return <Select.Option key={el?.value} label={el.label} value={el.value} />
})}
</Select>
) : (
<>
<TreeSelect
placeholder="请选择公司名称"
name="unitName"
dataSet={showData}
onOption={nodeCover}
onChange={handleChange}
style={{ marginRight: '7px' }}
popupCls={'down_select'}
/>
<Icon type="search" onClick={handleIconClick} style={{ cursor: 'pointer', fontSize: '20px' }} />
</>
)}
</div>
}
{messages.map((msg) => (
<div key={msg.id} className={`message-wrapper ${msg.sender}`}>
<div className="avatar">
<img src={msg.sender === 'user' ? user : chatGPT} alt="avatar" />
</div>
<div className="message">
{msg.errorTip ?? msg.text ?? '...'}
{msg.sender === 'bot' && msg.fileName && msg.finish === true && <div ><br />文档来源:{msg.fileName}<br /></div>}
{msg.files && msg.finish === true && msg.files?.map((item, index) => {
const url = `https://${item?.url}`
return <div key={index}>
{item?.content} <a href={url} target="_blank">查看文档</a>
</div>
})}
</div>
</div>
))}
</div>
<div className='tip'>
<div className='process'>当前进度:{process}</div>
{showButton ? <p className='stop' onClick={handleStopFetch}>停止生成</p> : ''}
</div>
<div className="input-box">
<input
type="text"
value={userInput}
onChange={(e) => setUserInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="请输入消息"
/>
{
<Tooltip title="请选择单位名称">
<Button color="primary" disabled={displayFlag === 'true' && !orgId} onClick={handleSendMessage}>
发送
</Button>
</Tooltip>
}
</div>
</div>
);
};
export default ChatApp;