feat(portal-case): 新增AI智能问答对话功能新增消息展示组件和发送消息组件,实现机器人与用户的消息交互。

支持打字机效果展示AI回复内容,并可显示思考过程与相关案例推荐。
添加对话框背景样式及自动滚动功能,优化用户体验。
提供开启新对话和推荐问题功能,增强交互性。
This commit is contained in:
陈昱达
2025-09-24 09:34:45 +08:00
parent d7e425ce9d
commit be411ec72d
5 changed files with 810 additions and 75 deletions

View File

@@ -0,0 +1,398 @@
<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 = ''
},
// 调用AI聊天接口
callAIChat(question) {
// 创建新的AI消息对象
const aiMessage = {
isBot: true,
text: '',
thinkText: '',
caseRefers: [], // 添加caseRefers字段
textCompleted: false // 添加文字处理完成状态默认为false
};
this.messageList.push(aiMessage);
// 模拟SSE流式响应
this.simulateSSE(question, aiMessage);
},
// 模拟SSE流式响应
simulateSSE(question, aiMessage) {
// 模拟AI思考过程包含think标签内容
const fullResponse =
"<think>" +
"让我思考一下如何更好地回答用户的问题。首先需要分析问题的关键点,然后查找相关的知识库内容。\n" +
"用户询问的是关于" + question + "的问题,我需要提供准确且有用的信息。\n" +
"</think>" +
"正在分析问题:" + question + "\n" +
"检索相关案例...\n" +
"匹配最佳解决方案...\n" +
"您好,我已经收到您的问题:\"" + question + "\"。\n" +
"正在分析相关内容...\n" +
"根据我的知识库,我为您提供以下解答:\n" +
"首先,我们需要了解问题的背景和关键点。\n" +
"在这个案例中,我们可以采用以下步骤来解决:\n" +
"1. 确认问题的具体表现和影响范围\n" +
"2. 分析可能的原因和影响因素\n" +
"3. 制定针对性的解决方案\n" +
"4. 实施并验证解决方案的有效性\n" +
"如果您还有其他相关问题,欢迎继续提问!";
let index = 0;
let inThinkTag = false;
let thinkContent = '';
let mainContent = '';
this.suggestions = [1,2,3]
this.$emit('update-suggestions', [1,2,3]);
// 模拟打字机效果
const typeNextCharacter = () => {
if (index < fullResponse.length) {
const char = fullResponse.charAt(index);
// 处理<think>标签
if (fullResponse.substring(index, index + 7) === '<think>') {
inThinkTag = true;
index += 7; // 跳过<think>标签
setTimeout(typeNextCharacter, 10);
return;
}
// 处理</think>标签
if (fullResponse.substring(index, index + 8) === '</think>') {
inThinkTag = false;
aiMessage.thinkText = thinkContent;
index += 8; // 跳过</think>标签
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
setTimeout(typeNextCharacter, 10);
return;
}
// 根据当前状态添加字符
if (inThinkTag) {
thinkContent += char;
aiMessage.thinkText = thinkContent;
} else {
mainContent += char;
aiMessage.text = mainContent;
}
index++;
setTimeout(typeNextCharacter, 30);
// 实时更新父组件的messageList
this.$emit('update-message', aiMessage);
} else {
// 设置文字处理完成状态为true
aiMessage.textCompleted = true;
// 模拟status 0的caseRefers数据
// 在消息完成后添加引用案例
setTimeout(() => {
aiMessage.caseRefers = [
{
caseId: "case_001",
title: "案例1标题",
authorName: "张三",
keywords: ["关键词1", "关键词2"],
content: "这是案例1的内容摘要..."
},
{
caseId: "case_002",
title: "案例2标题",
authorName: "李四",
keywords: ["关键词3", "关键词4"],
content: "这是案例2的内容摘要..."
}
];
this.$emit('update-message', aiMessage);
}, 500);
// 所有内容完成
this.$emit('loading', false);
}
};
// 开始打字机效果
setTimeout(typeNextCharacter, 100);
},
/*
// 真实的SSE实现暂时注释掉
callAIChat(question) {
// 创建新的AI消息对象
const aiMessage = {
isBot: true,
text: '',
thinkText: '',
caseRefers: [], // 添加caseRefers字段
textCompleted: false // 添加文字处理完成状态默认为false
};
this.messageList.push(aiMessage);
// 构造请求参数
const requestData = {
conversationId: this.conversationId,
query: question
};
// 使用fetch API处理SSE
const url = process.env.VUE_APP_BOE_BASE_API + '/xboe/m/boe/case/ai/chat';
const token = this.$store.getters.token;
// 创建POST请求
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[this.$ajax.tokenName]: token
},
body: JSON.stringify(requestData)
}).then(response => {
// 处理流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let accumulatedContent = ''; // 累积的内容用于打字机效果
let accumulatedThinkContent = ''; // 累积的思考内容
let inThinkSection = false; // 是否在思考部分
// 读取流数据
const read = () => {
reader.read().then(({ done, value }) => {
if (done) {
// 当流结束时设置文字处理完成状态为true
aiMessage.textCompleted = true;
this.$emit('update-message', aiMessage);
this.$emit('loading', false);
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(6));
// 根据status处理不同类型的数据
switch (jsonData.data.status) {
case 0: // 引用文件
// 处理引用文件信息
if (jsonData.data.fileRefer && jsonData.data.fileRefer.caseRefers) {
aiMessage.caseRefers = jsonData.data.fileRefer.caseRefers;
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
}
break;
case 1: // 流式对话内容
// 处理&lt;think&gt;标签内容
const content = jsonData.data.content;
if (content.startsWith('<think>')) {
inThinkSection = true;
accumulatedThinkContent = content.replace('&lt;think&gt;', '');
aiMessage.thinkText = accumulatedThinkContent;
} else if (content.startsWith('&lt;/think&gt;')) {
inThinkSection = false;
accumulatedThinkContent += content.replace('</think>', '');
aiMessage.thinkText = accumulatedThinkContent;
} else if (inThinkSection) {
accumulatedThinkContent += content;
aiMessage.thinkText = accumulatedThinkContent;
} else {
// 累积内容并更新显示
accumulatedContent += content;
aiMessage.text = accumulatedContent;
}
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
break;
case 2: // 回答完成
// 设置文字处理完成状态为true
aiMessage.textCompleted = true;
// 更新父组件的messageList
this.$emit('update-message', aiMessage);
break;
case 3: // 返回建议
// 这里可以处理建议问题
console.log('建议问题:', jsonData.data.suggestions);
this.$emit('update-suggestions', []);
break;
case 4: // 交互完成
this.$emit('loading', false);
break;
}
} catch (error) {
console.error('解析SSE数据错误:', error);
}
}
}
// 继续读取
read();
}).catch(error => {
console.error('SSE连接错误:', error);
// 出错时也设置文字处理完成状态
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.$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>