vue3实现ai聊天对话框功能
各功能部分学习
input输入
使用@keydown 键盘进行操作,回车和点击一样进行搜索
@keydown.enter.exact.prevent="handleSend" @keydown.enter.shift.exact="newline"
按钮 loading 加载图标:这里设置 template 插槽
<el-button type="primary" :loading="loading" @click="handleSend" > <template #icon> <el-icon><Position /></el-icon> </template> 发送 </el-button>
职责分离,子组件完成页面搭建,需要用到哪些逻辑,再通过 emit 通信到父组件由父组件完成。整个页面涉及到一个多个组件使用的 loading 属性,由 settings 仓库存储。
message对话框
简单的一行代码使用户消息放在右侧
&.message-user { flex-direction: row-reverse; //翻转实现用户布局在右侧 .message-content { align-items: flex-end; } }
换行属性white-space: pre-wrap;
保留源代码中的空白字符和换行符,否则为white-space: normal;
(默认值):合并连续的空白字符,忽略源代码中的换行符,自动换行
settings设置面板
- 设置属性都设置在仓库里,用于全局。
- 样式命名
w-full
这总可以通俗易懂的看出是宽度占满。 - 如何在 stlye 中修改 elementPlus 原本的样式。
- elementPlus 设置暗黑模式
//App.vue <template> <div :class="{ 'dark': isDarkMode }"> <router-view /> </div> </template> <script setup> import { computed } from 'vue' import { useSettingsStore } from './stores/settings' const settingsStore = useSettingsStore() const isDarkMode = computed(() => settingsStore.isDarkMode) </script> //dark.scss html.dark { // Element Plus 暗黑模式变量覆盖 --el-bg-color: var(--bg-color); --el-bg-color-overlay: var(--bg-color-secondary); --el-text-color-primary: var(--text-color-primary); --el-text-color-regular: var(--text-color-regular); --el-border-color: var(--border-color); // Element Plus 组件暗黑模式样式覆盖 .el-input-number { --el-input-number-bg-color: var(--bg-color-secondary); --el-input-number-text-color: var(--text-color-primary); } .el-select-dropdown { --el-select-dropdown-bg-color: var(--bg-color); --el-select-dropdown-text-color: var(--text-color-primary); } .el-slider { --el-slider-main-bg-color: var(--primary-color); } } //main.js import './assets/styles/dark.scss'
使用 scss 设置暗黑模式:
1. 使用pinia状态管理。
2. 在设置面板点击切换,触发toggleDarkMode动作(在pinia中设置),切换isDarlMode状态,并更新跟元素的data-theme属性。
3. 暗黑模式的样式设置在scss变量中
当刷新后样式会变回白天模式,但是这时候settings中选中的还是黑夜模式,这是为什么?
答:虽然设置存储在了 localStorage 中,但是在页面刷新后没有正确地初始化深色模式的样式。我们需要在应用启动时立即应用存储的主题设置。
解决办法:给 App.vue 加上挂载时就进行一次设置。
onMounted(() => { // 根据存储的设置初始化主题 document.documentElement.setAttribute('data-theme', settingsStore.isDarkMode ? 'dark' : 'light') })
传输数据
axios、 XMLHttpRequest 、fetch
以下是 Axios、XMLHttpRequest 和 Fetch 在发送 HTTP 请求时的对比,包括用法、性能、兼容性和适用场景的详细分析:
前后端通信所用技术对比
AI 对话需要到流式响应处理,通信技术最终采用 fetch。
场景 | Axios | XMLHttpRequest | Fetch |
---|---|---|---|
快速开发简单请求 | 优秀,语法简洁 | 不适合,代码繁琐 | 良好,语法简洁 |
全局配置需求 | 支持,提供 defaults 配置 | 不支持 | 需要手动实现 |
请求/响应拦截处理 | 原生支持拦截器 | 不支持 | 需要手动实现 |
上传/下载进度监听 | 不支持 | 支持 | 不支持 |
兼容旧版浏览器 | 支持,通过 polyfill | 支持 | 不支持 |
流式响应处理 | 不支持 | 不支持 | 支持(结合 ReadableStream ) |
轻量级需求 | 适合 | 不适合 | 适合 |
实现流式数据处理
1. 项目实现代码
- 发送请求
在 src/utils/api.js 中,chatApi.sendMessage 方法负责发送请求。根据 stream 参数的值,决定是否请求流式响应。
async sendMessage(messages, stream = false) { // ... const response = await fetch(`${API_BASE_URL}/chat/completions`, { method: 'POST', headers: { ...createHeaders(), ...(stream && { 'Accept': 'text/event-stream' }) // 如果是流式响应,添加相应的 Accept 头 }, body: JSON.stringify(payload) }) // ... if (stream) { return response // 对于流式响应,直接返回 response 对象 } // ... }
处理流式响应
在 src/utils/messageHandler.js 中,processStreamResponse 方法负责处理流式响应。它使用 ReadableStream 的 getReader 方法逐步读取数据,并使用 TextDecoder 解码数据。
- 读取流数据
- 解码数据块
- 处理解码后的数据:先拆分为行(数组),再转换为json字符串,再转换为js对象,提取出对象中content内容,更新message、更新token使用量
async processStreamResponse(response, { updateMessage, updateTokenCount }) { try { let fullResponse = ''; const reader = response.body.getReader(); const decoder = new TextDecoder(); // 1.读取流数据 while (true) { const { done, value } = await reader.read(); if (done) { console.log('流式响应完成'); break; } //2.解码数据块 const chunk = decoder.decode(value); //这里每一个chunk是一个可能包含多个数组 //3.处理解码后的数据,先拆分为行(数组),再转换为json字符串,再转换为js对象,提取出对象中content内容,更新message、更新token使用量 // 3.1 拆分为行 const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.includes('data: ')) { // 3.2 转换为json字符串 const jsonStr = line.replace('data: ', ''); // 检查是否结束 if (jsonStr === '[DONE]') { console.log('流式响应完成,读取完毕'); continue; } // 3.3 转换为js对象 try { const jsData = JSON.parse(jsonStr); if (jsData.choices[0].delta.content) { const content = jsData.choices[0].delta.content; //3.4 提取出对象中content内容,更新message fullResponse += content; updateMessage(fullResponse); } // 3.5更新token使用量 if (jsData.usage) { updateTokenCount(jsData.usage); } } catch (e) { console.error('解析JSON失败:', e); } } } } } catch (error) { console.error('流处理错误:', error); throw error; } },
更新界面
在 src/views/ChatView.vue 中,handleSend 方法调用 processStreamResponse,并通过回调函数 updateMessage 和 updateTokenCount 更新界面。
const handleSend = async (content) => { // ... try { const response = await chatApi.sendMessage( messages.value.slice(0, -1).map(m => ({ role: m.role, content: m.content })), settingsStore.streamResponse ); if (settingsStore.streamResponse) { // 这里使用了await不会将这个变化为同步吗,我了解到的使用await后会等之后的函数调用完再执行之后的代码,是这样吗? await messageHandler.processStreamResponse(response, { updateMessage: (content) => chatStore.updateLastMessage(content), updateTokenCount: (usage) => chatStore.updateTokenCount(usage) }); } // ... } catch (error) { chatStore.updateLastMessage('抱歉,发生了错误,请稍后重试。') } finally { chatStore.isLoading = false } }
2. 知识点
2.1. 疑问及解答
几个核心概念:1.ReadableStream,2.getReader()算是ReadableStream的一个方法吗,3.reader.read(),4.Uint8Array ,5. Streams API,6.TextDecoder
(由 fetch 返回的response.body 是ReadableStream对象引申出来的问题) fetch 处理响应有哪些方式?类型又是什么 ?
2.1.1. 解答的理解
有关流式涉及到的概念
ReadableStream
是 StreamsAPI 的核心对象 之一,这里涉及到是因为网络请求的response.body
是一个ReadableStream
对象。
深入补充:Streams API
是 Web API ,用于流方式处理数据 getReader()
是 ReadableStream
的一个方法,(因为 1 所以response.body
也有这个方法)
这个方法会返回一个 reader 对象(这里是简称),它可以逐块读取流中的数据,提供对流的完全控制 (注:使用 getReader 后,其他地方便无法访问流,意思是只有它返回的 reader 对象可以访问流)
reader 对象有一个方法reader.read()
异步读取流中的数据块 。返回值如下:
{ done: true/false, value: Uint8Array | undefined }
done
: 如果为true
,表示流已读取完毕。value
: 当前读取的块数据,通常是Uint8Array
, 每个元素占用 1 个字节 。
Uint8Array
是一种 类型化数组,高效地处理原始二进制数据,如文件块、图像、网络响应 。
对二进制数据的处理:
- 将
Uint8Array
转换为字符串,使用TextDecoder
,编码如utf-8
、utf-16
。使用它的decode
方法将字节数据转换为字符串。 - 转换为图像/视频,使用
Blob
,设置类型 image 或者 video,再将生成的 blob 对象转换为 URL,即可使用。
总结图示:
Streams API ├── ReadableStream(response.body类型) → 提供流式数据块 │ ├── getReader() → 获取 reader │ │ └── reader.read() → 读取单个数据块 {done, value} │ │ │ └── 数据块通常是 Uint8Array 类型 │ └── TextDecoder → 解码 Uint8Array 为字符串 └── Blob → 解码 Uint8Array 为图像视频
fetch 响应方法极其类型
方法 | 返回类型 | 常见场景 |
---|---|---|
response.text() | Promise<string> | 文本、HTML |
response.json() | Promise<Object> | JSON API 响应 |
response.blob() | Promise<Blob> | 图片、视频、文件下载 |
response.arrayBuffer() | Promise<ArrayBuffer> | 二进制数据、文件解析 |
response.formData() | Promise<FormData> | 表单响应(少见) |
response.body | ReadableStream | 实时处理、进度跟踪 |
注意:
fetch 的响应体只能被读取一次 (即:不能同时调用 response.json()
和 response.text()
等 )
即使响应有错误状态(如 404),fetch
不会抛出异常,需要手动检查 response.ok
。如下代码处理方式:
let response = await fetch('https://example.com'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
完整代码见:github
到此这篇关于vue3实现ai聊天对话框的文章就介绍到这了,更多相关vue3 ai聊天对话框内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
ElementUI 组件之Layout布局(el-row、el-col)
这篇文章主要介绍了ElementUI 组件之Layout布局(el-row、el-col),本文通过实例代码图文相结合给大家介绍的非常详细,感兴趣的朋友一起看看吧2024-07-07vue mounted周期中document.querySelectorAll()获取不到元素的解决
这篇文章主要介绍了vue mounted周期中document.querySelectorAll()获取不到元素的解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2024-03-03Element UI中v-infinite-scroll无限滚动组件使用详解
在移动端数据的更新中许多方法孕育而生,无限滚轮也是解决的方案一种,Element-ui为vue开发了一个事件(v-infinite-scroll),下面这篇文章主要给大家介绍了关于Element UI中v-infinite-scroll无限滚动组件使用的相关资料,需要的朋友可以参考下2023-02-02vue-awesome-swiper 基于vue实现h5滑动翻页效果【推荐】
说到h5的翻页,很定第一时间想到的是swiper。但是我当时想到的却是,vue里边怎么用swiper。这篇文章主要介绍了vue-awesome-swiper - 基于vue实现h5滑动翻页效果 ,需要的朋友可以参考下2018-11-11深入理解vue.js中$watch的oldvalue与newValue
这篇文章主要给大家介绍了关于vue.js中$watch的oldvalue与newValue的相关资料,文中通过示例代码介绍的非常详细,并且介绍了关于watch的其他测试,对大家学习或者使用vue.js具有一定的参考学习价值,需要的朋友们下面跟着小编来一起看看吧。2017-08-08
最新评论