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: '',
});
const [process, setProcess] = useState([]);
const controllerRef = useRef(new AbortController());
const [orgId, setOrgId] = useState<string>();
const [userId, setUserId] = useState('')
const [selectValue, setSelectValue] = useState()
const [selectionOptions, setSelectionOptions] = useState([])
const [isSelectVisible, setIsSelectVisible] = useState(false);
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);
const content = messageData?.message?.content;
currentBotMessageRef.current.text += content;
if (messageData?.message?.features && messageData?.message?.features?.doc_citations) {
let fileInfoString = '';
messageData?.message?.features?.doc_citations.forEach(citation => {
citation?.documents && citation?.documents.forEach((item) => {
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);
setUserInput('');
}
};
const handleStopFetch = () => {
if (controllerRef.current) {
controllerRef.current.abort();
}
}
const nodeCover = ({ record }) => {
const nodeProps = {
title: record.get('unitName'),
};
if (record.get('power') === 'false') {
nodeProps.disabled = true;
}
return nodeProps;
}
const handleChange = (val) => {
setOrgId(val)
}
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, },
]);
} 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);
}
};
console.log(displayFlag, 'displayFlag');
return (
<div className="chat-container">
<div className="+">
{
displayFlag === 'true' && <div className='search-box'>
{isSelectVisible ? (
<Select
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;
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
background-color: rgb(230, 230, 230);
font-family: Arial, sans-serif;
overflow: auto;
:global {
.el-select .el-input__inner {
height: 27px;
width: 183px;
margin-right: 27px;
font-size: 12px;
border-color: #e6e6e6;
border-radius: 0;
}
}
}
.chat-box {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
background-color: #ffffff;
}
.message-wrapper {
display: flex;
gap: 10px;
}
.message-wrapper.user {
flex-direction: row-reverse;
}
.message-wrapper.bot {
justify-content: flex-start;
}
.message {
padding: 10px 14px;
border-radius: 12px;
background-color: #007bff;
color: #ffffff;
word-break: break-word;
white-space: pre-wrap;
}
.message-wrapper.bot .message {
background-color: #f1f1f1;
color: #333333;
}
.search-box {
position: sticky;
top: 0;
margin: 10px;
margin-left: auto;
z-index: 9999;
background-color: #007bff;
padding: 5px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.tip {
display: flex;
justify-content: space-between;
.process {
color: #999;
font-size: 16px;
margin: 10px;
margin-left: 20px;
}
.stop {
color: #0078d4;
font-size: 16px;
margin: 10px;
cursor: pointer;
margin-right: 20px;
}
}
.input-box {
position: sticky;
bottom: 0;
display: flex;
justify-content: space-between;
padding: 20px;
background-color: #fff;
border-top: 1px solid #e0e0e0;
}
.input-box input {
flex: 1;
padding: 4px;
border-radius: 20px;
border: 1px solid #ccc;
margin-right: 14px;
font-size: 14px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 10px;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
:global {
.el-select-dropdown {
top: 33px !important;
}
.down_select {
.c7n-tree-treenode {
.c7n-tree-node-content-wrapper {
.c7n-tree-title {
color: #007bff !important;
}
}
}
.c7n-tree-treenode-disabled {
.c7n-tree-node-content-wrapper {
.c7n-tree-title {
color: #d9d9d9 !important;
}
}
}
}
.aipage_form {
.c7n-pro-field-label {
width: 35px !important;
color: #333333 !important;
}
}
}