mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-12 12:26:44 +08:00
feat(portal-case): 新增AI智能问答对话功能新增消息展示组件和发送消息组件,实现机器人与用户的消息交互。
支持打字机效果展示AI回复内容,并可显示思考过程与相关案例推荐。 添加对话框背景样式及自动滚动功能,优化用户体验。 提供开启新对话和推荐问题功能,增强交互性。
This commit is contained in:
398
src/views/portal/case/components/sendMessage.vue
Normal file
398
src/views/portal/case/components/sendMessage.vue
Normal 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: // 流式对话内容
|
||||
// 处理<think>标签内容
|
||||
const content = jsonData.data.content;
|
||||
if (content.startsWith('<think>')) {
|
||||
inThinkSection = true;
|
||||
accumulatedThinkContent = content.replace('<think>', '');
|
||||
aiMessage.thinkText = accumulatedThinkContent;
|
||||
} else if (content.startsWith('</think>')) {
|
||||
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>
|
||||
Reference in New Issue
Block a user