mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-13 04:46:44 +08:00
添加 Accept : EventStream 请求头
This commit is contained in:
388
src/views/portal/case/components/sendMessage.vue
Normal file
388
src/views/portal/case/components/sendMessage.vue
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
<template>
|
||||||
|
<div class="input-area">
|
||||||
|
<el-input
|
||||||
|
v-model="inputContent"
|
||||||
|
class="input-placeholder"
|
||||||
|
placeholder="有问题,尽管问"
|
||||||
|
@keyup.enter.native="handleSend"
|
||||||
|
:disabled="disabled"
|
||||||
|
></el-input>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button type="primary" size="small" class="start-btn" @click="handleNewConversation">
|
||||||
|
+ 开启新对话
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" class="send-btn" @click="handleSend" :disabled="disabled">
|
||||||
|
<i class="el-icon-s-promotion"></i>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { aiChat } from '@/api/boe/aiChat.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SendMessage',
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
messageList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
suggestions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
inputContent: this.value,
|
||||||
|
conversationId: '' // 会话ID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
this.inputContent = newVal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSend() {
|
||||||
|
if (!this.inputContent.trim() || this.disabled) return
|
||||||
|
// 添加用户消息到列表
|
||||||
|
const userMessage = {
|
||||||
|
isBot: false,
|
||||||
|
text: this.inputContent
|
||||||
|
};
|
||||||
|
this.messageList.push(userMessage);
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
this.$emit('loading', true);
|
||||||
|
|
||||||
|
// 调用AI聊天接口 (暂时注释掉SSE,使用模拟数据)
|
||||||
|
this.callAIChat(this.inputContent);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
this.inputContent = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// 真实的SSE实现(暂时注释掉)
|
||||||
|
callAIChat(question) {
|
||||||
|
// 创建新的AI消息对象
|
||||||
|
const aiMessage = {
|
||||||
|
isBot: true,
|
||||||
|
text: '',
|
||||||
|
status:null,
|
||||||
|
thinkText: '',
|
||||||
|
caseRefers: [], // 添加caseRefers字段
|
||||||
|
textCompleted: false // 添加文字处理完成状态,默认为false
|
||||||
|
};
|
||||||
|
this.messageList.push(aiMessage);
|
||||||
|
|
||||||
|
// 构造请求参数
|
||||||
|
const requestData = {
|
||||||
|
conversationId: this.conversationId,
|
||||||
|
query: question
|
||||||
|
};
|
||||||
|
// 创建POST请求
|
||||||
|
fetch('/systemapi/xboe/m/boe/case/ai/chat',{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
"accept": "text/event-stream",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
}).then(r=>{
|
||||||
|
return r
|
||||||
|
}).then(response => {
|
||||||
|
// 处理流式响应
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
let buffer = '';
|
||||||
|
let accumulatedContent = ''; // 累积的内容用于打字机效果
|
||||||
|
let accumulatedThinkContent = ''; // 累积的思考内容
|
||||||
|
let inThinkSection = false; // 是否在思考部分
|
||||||
|
let typingTimer = null; // 打字机定时器
|
||||||
|
let thinkTypingTimer = null; // 思考内容打字机定时器
|
||||||
|
|
||||||
|
// 逐字显示文本的函数
|
||||||
|
const typeText = (message, fullContent) => {
|
||||||
|
// 如果已有定时器在运行,先清除它
|
||||||
|
if (typingTimer) {
|
||||||
|
clearInterval(typingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前已显示的文本长度
|
||||||
|
const currentLength = message.text.length;
|
||||||
|
// 获取完整文本
|
||||||
|
const targetLength = fullContent.length;
|
||||||
|
|
||||||
|
// 如果已经显示完整文本,不需要继续
|
||||||
|
if (currentLength >= targetLength) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typingSpeed = 50; // 每个字符的间隔时间(毫秒)
|
||||||
|
|
||||||
|
typingTimer = setInterval(() => {
|
||||||
|
// 计算下一个要显示的字符索引
|
||||||
|
const nextIndex = message.text.length + 1;
|
||||||
|
if (nextIndex <= targetLength) {
|
||||||
|
message.text = fullContent.substring(0, nextIndex);
|
||||||
|
this.$emit('update-message', message);
|
||||||
|
} else {
|
||||||
|
clearInterval(typingTimer);
|
||||||
|
typingTimer = null;
|
||||||
|
// 当打字机效果完成时,检查是否应该设置textCompleted为true
|
||||||
|
// 这应该在status 4(交互完成)时才设置
|
||||||
|
if (message.status === 4) {
|
||||||
|
if (nextIndex >= targetLength) {
|
||||||
|
message.textCompleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, typingSpeed);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 逐字显示思考内容的函数
|
||||||
|
const typeThinkText = (message, fullThinkContent) => {
|
||||||
|
// 如果已有定时器在运行,先清除它
|
||||||
|
if (thinkTypingTimer) {
|
||||||
|
clearInterval(thinkTypingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前已显示的文本长度
|
||||||
|
const currentLength = message.thinkText.length;
|
||||||
|
// 获取完整文本
|
||||||
|
const targetLength = fullThinkContent.length;
|
||||||
|
|
||||||
|
// 如果已经显示完整文本,不需要继续
|
||||||
|
if (currentLength >= targetLength) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从当前显示位置开始继续显示(避免清空重显)
|
||||||
|
const startIndex = currentLength;
|
||||||
|
|
||||||
|
const typingSpeed = 20; // 每个字符的间隔时间(毫秒)
|
||||||
|
|
||||||
|
thinkTypingTimer = setInterval(() => {
|
||||||
|
// 计算下一个要显示的字符索引
|
||||||
|
const nextIndex = message.thinkText.length + 1;
|
||||||
|
if (nextIndex <= targetLength) {
|
||||||
|
message.thinkText = fullThinkContent.substring(0, nextIndex);
|
||||||
|
this.$emit('update-message', message);
|
||||||
|
} else {
|
||||||
|
clearInterval(thinkTypingTimer);
|
||||||
|
thinkTypingTimer = null;
|
||||||
|
}
|
||||||
|
}, typingSpeed);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加一个检查是否所有文本都已完成显示的函数
|
||||||
|
const isTextDisplayCompleted = (message, fullContent) => {
|
||||||
|
return message.text.length >= fullContent.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 读取流数据
|
||||||
|
const read = () => {
|
||||||
|
reader.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
// 当流结束时,等待打字机效果完成
|
||||||
|
const waitForTyping = () => {
|
||||||
|
if (!typingTimer) {
|
||||||
|
this.$emit('loading', false);
|
||||||
|
} else {
|
||||||
|
setTimeout(waitForTyping, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
waitForTyping();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码数据
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// 按行分割数据
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop(); // 保留不完整的行
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data:')) {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(line.substring(5));
|
||||||
|
// 根据status处理不同类型的数据
|
||||||
|
switch (jsonData.status) {
|
||||||
|
case 0: // 引用文件
|
||||||
|
// 处理引用文件信息
|
||||||
|
if (jsonData.fileRefer && jsonData.fileRefer.caseRefers) {
|
||||||
|
aiMessage.caseRefers = jsonData.fileRefer.caseRefers;
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
}
|
||||||
|
// 从响应中获取并保存conversationId
|
||||||
|
if (jsonData.conversationId) {
|
||||||
|
this.conversationId = jsonData.conversationId;
|
||||||
|
sessionStorage.setItem('conversationId', jsonData.conversationId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // 流式对话内容
|
||||||
|
// 处理
|
||||||
|
const content = jsonData.content;
|
||||||
|
aiMessage.hasThink = false;
|
||||||
|
if (content.startsWith('<think>')) {
|
||||||
|
aiMessage.hasThink = true
|
||||||
|
inThinkSection = true;
|
||||||
|
accumulatedThinkContent = content.replace('<think>', '');
|
||||||
|
// 使用打字机效果显示think内容
|
||||||
|
typeThinkText(aiMessage, accumulatedThinkContent);
|
||||||
|
} else if (content.startsWith('</think>')) {
|
||||||
|
inThinkSection = false;
|
||||||
|
accumulatedThinkContent += content.replace('</think>', '');
|
||||||
|
// 使用打字机效果显示think内容
|
||||||
|
typeThinkText(aiMessage, accumulatedThinkContent);
|
||||||
|
} else if (inThinkSection) {
|
||||||
|
accumulatedThinkContent += content;
|
||||||
|
// 使用打字机效果显示think内容
|
||||||
|
typeThinkText(aiMessage, accumulatedThinkContent);
|
||||||
|
} else {
|
||||||
|
// 累积内容并使用打字机效果更新显示
|
||||||
|
accumulatedContent += content;
|
||||||
|
// 如果thinkText已经显示完整,则继续使用打字机效果显示内容
|
||||||
|
if( aiMessage.hasThink){
|
||||||
|
if(aiMessage.thinkText.length >=accumulatedThinkContent.length){
|
||||||
|
typeText(aiMessage, accumulatedContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
typeText(aiMessage, accumulatedContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// 不在这里直接更新,让打字机效果处理更新
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // 回答完成
|
||||||
|
// 不再在这里设置textCompleted状态
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
// 从响应中获取并保存conversationId
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3: // 返回建议
|
||||||
|
// 这里可以处理建议问题
|
||||||
|
this.$emit('update-suggestions', jsonData.suggestions);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4: // 交互完成
|
||||||
|
aiMessage.status = 4
|
||||||
|
|
||||||
|
// 从响应中获取并保存conversationId
|
||||||
|
this.$emit('loading', false);
|
||||||
|
// 检查文本是否已经完全显示,如果是则设置textCompleted为true
|
||||||
|
if (isTextDisplayCompleted(aiMessage, accumulatedContent)) {
|
||||||
|
// aiMessage.textCompleted = true;
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析SSE数据错误:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续读取
|
||||||
|
read();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('SSE连接错误:', error);
|
||||||
|
// 出错时也设置文字处理完成状态
|
||||||
|
if (typingTimer) {
|
||||||
|
clearInterval(typingTimer);
|
||||||
|
typingTimer = null;
|
||||||
|
}
|
||||||
|
aiMessage.textCompleted = true;
|
||||||
|
this.$emit('loading', false);
|
||||||
|
aiMessage.text = '当前无法获取回答,请稍后重试';
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始读取数据
|
||||||
|
read();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
// 出错时也设置文字处理完成状态
|
||||||
|
aiMessage.textCompleted = true;
|
||||||
|
this.$emit('loading', false);
|
||||||
|
aiMessage.text = '当前无法获取回答,请稍后重试';
|
||||||
|
// 更新父组件的messageList
|
||||||
|
this.$emit('update-message', aiMessage);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleNewConversation() {
|
||||||
|
this.conversationId = ''
|
||||||
|
this.$emit('new-conversation')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.input-area {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 5px 16px 10px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.input-placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
::v-deep .el-input__inner {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #409eff;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #409eff;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user