在现代 web 应用中,集成智能对话功能已经成为提升用户体验的重要手段之一。本文将介绍如何通过 Vue 3 和 Element Plus 构建一个高效的 AI 聊天助手界面,并详细讲解其实现原理和功能。
1. 整体架构
该聊天界面分为 左侧边栏 和 右侧内容区域,实现了清晰的布局结构,左侧边栏用于展示历史会话和各种功能开关,右侧内容区域用于展示当前会话的消息和输入框。系统支持以下功能:
- 向量存储:可以开启以便访问已上传的文档内容。
- Agent功能:允许开启自动化代理功能来增强对话能力。
- 数据分析:用户可以上传文件进行分析,并在分析后通过向量存储访问文件内容。
此外,用户还可以查看和管理历史会话、快速开始对话、上传文件等。
2. 左侧边栏:管理与功能开关
左侧边栏包含了三个主要部分:
- Logo与新建会话:展示应用的Logo并支持创建新会话。
- 功能开关:包括启用向量存储、启用Agent功能以及文件上传进行数据分析的功能开关。这些功能开关都使用了 Element Plus 的
el-switch
组件,允许用户根据需求自由开关。 - 历史消息列表:展示用户的历史会话,用户可以点击每个会话查看之前的消息,并删除不需要的会话。
<div class="sidebar">
<div class="sidebar-header">
<div class="logo">
<img src="@/assets/logo.png" alt="Logo" class="logo-img" />
<span class="logo-text">AI助手</span>
</div>
<el-icon class="new-chat-icon" @click="resetChat"><Plus /></el-icon>
</div>
<div class="feature-toggles">
<div class="toggle-item">
<div class="toggle-label-group">
<span class="toggle-label">向量存储</span>
<el-tooltip content="开启后可以访问已上传的文档内容" placement="right">
<el-icon class="info-icon"><InfoFilled /></el-icon>
</el-tooltip>
</div>
<el-switch v-model="enableVectorStore" size="small" active-color="#409EFF" />
</div>
<div class="toggle-item">
<span class="toggle-label">Agent</span>
<el-switch v-model="enableAgent" size="small" active-color="#409EFF" />
</div>
<div class="toggle-item">
<div class="toggle-label-group">
<span class="toggle-label">数据分析</span>
<el-tooltip content="上传文件进行分析,分析后可通过向量存储访问文件内容" placement="right">
<el-icon class="info-icon"><InfoFilled /></el-icon>
</el-tooltip>
</div>
<el-upload :auto-upload="false" :show-file-list="false" :on-change="handleEmbedding" class="upload-btn-small">
<el-button size="small" type="primary" :loading="uploadLoading">上传文件</el-button>
</el-upload>
</div>
</div>
<div class="history-list">
<div class="history-group">
<div class="group-title">历史消息</div>
<div v-for="item in conversations" :key="item.sessionId" class="chat-item" :class="{ active: currentSessionId === item.sessionId }" @click="selectSession(item)">
<div class="chat-item-content">
<el-icon><ChatLineRound /></el-icon>
<span class="chat-title">{
{ item.name }}</span>
<span class="chat-time">{
{ formatTime(item.createdTime) }}</span>
</div>
<div class="chat-item-actions">
<el-icon class="delete-icon" @click.stop="handleDelete(item.sessionId)"><Delete /></el-icon>
</div>
</div>
</div>
</div>
</div>
3. 右侧内容区:展示与互动
右侧内容区展示当前选定会话的详细内容。用户可以看到与 AI 的对话消息,并通过输入框进行新一轮的对话。输入框支持文本输入和文件上传,允许用户在对话中插入文件或直接输入消息内容。
初始页面
当用户没有选择当前会话时,右侧内容区域会显示欢迎信息和热门问答,供用户快速开始与 AI 的对话。
<template v-if="!currentSessionId">
<div class="welcome-page">
<h1 class="welcome-title">欢迎使用小鹏AI助手</h1>
<div class="history-qa" v-if="randomHistoryMessages.length > 0">
<h2>热门问答</h2>
<div class="qa-list">
<div v-for="(qa, index) in randomHistoryMessages" :key="index" class="qa-item" @click="quickStart(qa.question)">
<div class="qa-question">
<el-icon><ChatLineRound /></el-icon>
<span>{
{ qa.question }}</span>
</div>
<div class="qa-answer">{
{ qa.answer }}</div>
</div>
</div>
</div>
</div>
</template>
对话消息区
当用户选择了某个会话后,右侧内容区展示历史对话消息,包括用户和 AI 的消息内容。每条消息根据消息类型(如用户消息、AI 回复、系统消息)进行不同的样式渲染。
<template v-else>
<div class="chat-messages" ref="messagesContainer">
<div v-for="message in messages" :key="message.id" class="message-item" :class="message.type.toLowerCase()">
<div class="message-content">{
{ message.textContent }}</div>
<div class="message-time">{
{ formatMessageTime(message.createdTime) }}</div>
</div>
</div>
</template>
输入与发送
输入框允许用户在对话框内输入消息,并支持文件上传。点击发送按钮后,消息将被发送到后端,并实时显示在聊天界面。
<div class="input-section">
<div class="input-wrapper">
<el-input v-model="inputMessage" :placeholder="currentSessionId ? '继续对话...' : '给 AI助手 发送消息'" class="message-input" size="large" @keyup.enter="sendMessage">
<template #suffix>
<div class="input-actions">
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :on-change="handleFileChange" class="upload-btn">
<el-icon :size="20"><Paperclip /></el-icon>
</el-upload>
<div class="send-btn" :class="{ active: canSend }" @click="sendMessage">
<el-icon><Position /></el-icon>
</div>
</div>
</template>
</el-input>
</div>
<div v-if="selectedFile" class="selected-file">
<el-tag closable @close="clearFile" class="file-tag">{
{ selectedFile.name }}</el-tag>
</div>
</div>
4. 数据与状态管理
在代码中,使用了 Vue 3 的 Composition API 来管理组件的状态和响应式数据。这里的状态主要包括用户输入、当前会话、消息内容、文件上传等。
const inputMessage = ref('') // 用户输入的消息
const conversations = ref([]) // 历史会话列表
const currentSessionId = ref(null) // 当前选中的会话 ID
const messages = ref([]) // 当前会话的消息内容
const loading = ref(false) // 加载状态,用于发送消息时显示加载效果
const messagesContainer = ref(null) // 对话内容容器,用于滚动
const uploadRef = ref(null) // 文件上传组件的引用
const selectedFile = ref(null) // 已选择的文件
const eventSource = ref(null) // 用于 SSE 连接的对象
const enableVectorStore = ref(false) // 启用向量存储
const enableAgent = ref(false) // 启用 Agent
const uploadLoading = ref(false) // 文件上传时的加载状态
const randomHistoryMessages = ref([]) // 随机历史消息
5. 历史会话与消息获取
const getConversations = async () => {
try {
const res = await request({
url: '/api/ai/getConversationById',
method: 'get',
params: { userId: userStore.userId }
})
if (res.success) {
conversations.value = res.data
// 获取随机历史消息
await getRandomHistoryMessages()
} else {
ElMessage.error('获取历史会话失败')
}
} catch (error) {
console.error('获取历史会话出错:', error)
ElMessage.error('获取历史会话失败')
}
}
getConversations
方法用于从后端获取历史会话记录,并通过 conversations
状态更新会话列表。如果获取失败,会显示错误信息。
同理,getMessages
用来获取特定会话的消息内容。
6. 消息发送与文件上传
const sendMessage = async () => {
if ((!inputMessage.value.trim() && !selectedFile.value) || loading.value) return
const content = inputMessage.value.trim()
inputMessage.value = '' // 立即清空输入框
loading.value = true
try {
let sessionId = currentSessionId.value
// 如果没有当前会话,创建新会话
if (!sessionId) {
sessionId = await createConversation(content || selectedFile.value?.name)
if (!sessionId) {
loading.value = false
return
}
currentSessionId.value = sessionId
await getConversations()
}
// 准备消息数据
const chatMessage = {
sessionId: sessionId.toString(),
textContent: content,
type: 'user',
medias: [],
userId: userStore.userId
}
// 添加用户消息到界面
messages.value.push({
...chatMessage,
createdTime: new Date().toISOString()
})
// 添加AI消息占位
const aiMessage = {
id: (Date.now() + 1).toString(),
type: 'assistant',
textContent: '',
createdTime: new Date().toISOString(),
sessionId: sessionId.toString()
}
messages.value.push(aiMessage)
await scrollToBottom()
// 创建 FormData
const formData = new FormData()
formData.append('input', JSON.stringify({
message: chatMessage,
params: {
enableVectorStore: enableVectorStore.value,
enableAgent: enableAgent.value
}
}))
// 如果有文件,添加到 FormData
if (selectedFile.value) {
formData.append('file', selectedFile.value)
}
// 关闭之前的连接
if (eventSource.value) {
eventSource.value.close()
}
// 创建 SSE 连接
eventSource.value = new SSE('/api/ai/chat', {
headers: {
Accept: 'text/event-stream',
},
method: 'POST',
payload: formData
})
eventSource.value.onmessage = (event) => {
try {
const responseData = JSON.parse(event.data)
const finishReason = responseData.result?.metadata?.finishReason
if (responseData.result?.output?.content) {
aiMessage.textContent += responseData.result.output.content
messages.value = [...messages.value]
scrollToBottom()
}
if (finishReason && finishReason.toLowerCase() === 'stop') {
// 保存ai的回复消息
saveMessage({
sessionId: aiMessage.sessionId,
textContent: aiMessage.textContent,
type: aiMessage.type,
medias: []
})
loading.value = false
clearFile()
eventSource.value?.close()
}
} catch (error) {
console.error('解析消息出错:', error)
}
}
eventSource.value.onerror = (error) => {
console.error('SSE 连接错误:', error)
if (aiMessage.textContent === '') {
aiMessage.textContent = '消息处理出错,请重试'
}
eventSource.value?.close()
loading.value = false
clearFile()
}
eventSource.value.onopen = () => {
console.log('SSE 连接已建立')
}
} catch (error) {
console.error('发送消息失败:', error)
ElMessage.error('发送消息失败,请重试')
loading.value = false
}
}
sendMessage
是处理用户输入的核心函数。当用户输入消息并点击发送时,函数首先判断是否有有效的输入(文本或文件),然后处理消息的发送,包括创建新的会话、更新界面上的消息以及建立与后端的 SSE(服务器推送事件)连接。
SSE 连接的目的是接收 AI 模型的响应,并实时更新对话内容。AI 回复的内容通过 onmessage
事件监听器获取,并动态地将其添加到消息列表中。
7. 文件上传与处理
const handleEmbedding = async (file) => {
uploadLoading.value = true
try {
const formData = new FormData()
formData.append('file', file.raw)
const res = await request({
url: '/api/ai/embedding',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (res.success) {
ElMessage.success('文件分析成功,现在可以开启向量存储来访问文件内容')
// 自动开启向量存储
enableVectorStore.value = true
} else {
ElMessage.error('文件分析失败')
}
} catch (error) {
console.error('文件分析失败:', error)
ElMessage.error('文件分析失败')
} finally {
uploadLoading.value = false
}
}
handleEmbedding
方法处理文件上传。文件被上传到后端进行分析,分析后开启向量存储功能,用户可以通过此功能访问文件中的内容。上传过程使用了 FormData
来处理文件数据,并通过 multipart/form-data
请求头发送。
8. 滚动和视图更新
const scrollToBottom = async () => {
await nextTick()
if (messagesContainer.value) {
const scrollOptions = {
top: messagesContainer.value.scrollHeight,
behavior: 'smooth'
}
messagesContainer.value.scrollTo(scrollOptions)
}
}
scrollToBottom
方法确保消息列表在更新后自动滚动到最新消息。这是通过访问 messagesContainer
的 DOM 节点,并设置其 scrollTop
属性实现的。
总结
本系统利用 Vue 3 和 Element Plus 实现了一个高效、互动性强的 AI 聊天助手界面。通过集成历史消息管理、文件上传、向量存储等功能,提供了一个全面的对话平台,满足了用户在日常工作中的各种需求。