vue3實現(xiàn)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> 發(fā)送 </el-button>
職責分離,子組件完成頁面搭建,需要用到哪些邏輯,再通過 emit 通信到父組件由父組件完成。整個頁面涉及到一個多個組件使用的 loading 屬性,由 settings 倉庫存儲。
message對話框
簡單的一行代碼使用戶消息放在右側(cè)
&.message-user { flex-direction: row-reverse; //翻轉(zhuǎn)實現(xiàn)用戶布局在右側(cè) .message-content { align-items: flex-end; } }
換行屬性white-space: pre-wrap;
保留源代碼中的空白字符和換行符,否則為white-space: normal;
(默認值):合并連續(xù)的空白字符,忽略源代碼中的換行符,自動換行
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狀態(tài)管理。
2. 在設置面板點擊切換,觸發(fā)toggleDarkMode動作(在pinia中設置),切換isDarlMode狀態(tài),并更新跟元素的data-theme屬性。
3. 暗黑模式的樣式設置在scss變量中
當刷新后樣式會變回白天模式,但是這時候settings中選中的還是黑夜模式,這是為什么?
答:雖然設置存儲在了 localStorage 中,但是在頁面刷新后沒有正確地初始化深色模式的樣式。我們需要在應用啟動時立即應用存儲的主題設置。
解決辦法:給 App.vue 加上掛載時就進行一次設置。
onMounted(() => { // 根據(jù)存儲的設置初始化主題 document.documentElement.setAttribute('data-theme', settingsStore.isDarkMode ? 'dark' : 'light') })
傳輸數(shù)據(jù)
axios、 XMLHttpRequest 、fetch
以下是 Axios、XMLHttpRequest 和 Fetch 在發(fā)送 HTTP 請求時的對比,包括用法、性能、兼容性和適用場景的詳細分析:
前后端通信所用技術對比
AI 對話需要到流式響應處理,通信技術最終采用 fetch。
場景 | Axios | XMLHttpRequest | Fetch |
---|---|---|---|
快速開發(fā)簡單請求 | 優(yōu)秀,語法簡潔 | 不適合,代碼繁瑣 | 良好,語法簡潔 |
全局配置需求 | 支持,提供 defaults 配置 | 不支持 | 需要手動實現(xiàn) |
請求/響應攔截處理 | 原生支持攔截器 | 不支持 | 需要手動實現(xiàn) |
上傳/下載進度監(jiān)聽 | 不支持 | 支持 | 不支持 |
兼容舊版瀏覽器 | 支持,通過 polyfill | 支持 | 不支持 |
流式響應處理 | 不支持 | 不支持 | 支持(結(jié)合 ReadableStream ) |
輕量級需求 | 適合 | 不適合 | 適合 |
實現(xiàn)流式數(shù)據(jù)處理
1. 項目實現(xiàn)代碼
- 發(fā)送請求
在 src/utils/api.js 中,chatApi.sendMessage 方法負責發(fā)送請求。根據(jù) stream 參數(shù)的值,決定是否請求流式響應。
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 方法逐步讀取數(shù)據(jù),并使用 TextDecoder 解碼數(shù)據(jù)。
- 讀取流數(shù)據(jù)
- 解碼數(shù)據(jù)塊
- 處理解碼后的數(shù)據(jù):先拆分為行(數(shù)組),再轉(zhuǎn)換為json字符串,再轉(zhuǎn)換為js對象,提取出對象中content內(nèi)容,更新message、更新token使用量
async processStreamResponse(response, { updateMessage, updateTokenCount }) { try { let fullResponse = ''; const reader = response.body.getReader(); const decoder = new TextDecoder(); // 1.讀取流數(shù)據(jù) while (true) { const { done, value } = await reader.read(); if (done) { console.log('流式響應完成'); break; } //2.解碼數(shù)據(jù)塊 const chunk = decoder.decode(value); //這里每一個chunk是一個可能包含多個數(shù)組 //3.處理解碼后的數(shù)據(jù),先拆分為行(數(shù)組),再轉(zhuǎn)換為json字符串,再轉(zhuǎn)換為js對象,提取出對象中content內(nèi)容,更新message、更新token使用量 // 3.1 拆分為行 const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.includes('data: ')) { // 3.2 轉(zhuǎn)換為json字符串 const jsonStr = line.replace('data: ', ''); // 檢查是否結(jié)束 if (jsonStr === '[DONE]') { console.log('流式響應完成,讀取完畢'); continue; } // 3.3 轉(zhuǎn)換為js對象 try { const jsData = JSON.parse(jsonStr); if (jsData.choices[0].delta.content) { const content = jsData.choices[0].delta.content; //3.4 提取出對象中content內(nèi)容,更新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 方法調(diào)用 processStreamResponse,并通過回調(diào)函數(shù) 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后會等之后的函數(shù)調(diào)用完再執(zhí)行之后的代碼,是這樣嗎? await messageHandler.processStreamResponse(response, { updateMessage: (content) => chatStore.updateLastMessage(content), updateTokenCount: (usage) => chatStore.updateTokenCount(usage) }); } // ... } catch (error) { chatStore.updateLastMessage('抱歉,發(fā)生了錯誤,請稍后重試。') } 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 的核心對象 之一,這里涉及到是因為網(wǎng)絡請求的response.body
是一個ReadableStream
對象。
深入補充:Streams API
是 Web API ,用于流方式處理數(shù)據(jù) getReader()
是 ReadableStream
的一個方法,(因為 1 所以response.body
也有這個方法)
這個方法會返回一個 reader 對象(這里是簡稱),它可以逐塊讀取流中的數(shù)據(jù),提供對流的完全控制 (注:使用 getReader 后,其他地方便無法訪問流,意思是只有它返回的 reader 對象可以訪問流)
reader 對象有一個方法reader.read()
異步讀取流中的數(shù)據(jù)塊 。返回值如下:
{ done: true/false, value: Uint8Array | undefined }
done
: 如果為true
,表示流已讀取完畢。value
: 當前讀取的塊數(shù)據(jù),通常是Uint8Array
, 每個元素占用 1 個字節(jié) 。
Uint8Array
是一種 類型化數(shù)組,高效地處理原始二進制數(shù)據(jù),如文件塊、圖像、網(wǎng)絡響應 。
對二進制數(shù)據(jù)的處理:
- 將
Uint8Array
轉(zhuǎn)換為字符串,使用TextDecoder
,編碼如utf-8
、utf-16
。使用它的decode
方法將字節(jié)數(shù)據(jù)轉(zhuǎn)換為字符串。 - 轉(zhuǎn)換為圖像/視頻,使用
Blob
,設置類型 image 或者 video,再將生成的 blob 對象轉(zhuǎn)換為 URL,即可使用。
總結(jié)圖示:
Streams API ├── ReadableStream(response.body類型) → 提供流式數(shù)據(jù)塊 │ ├── getReader() → 獲取 reader │ │ └── reader.read() → 讀取單個數(shù)據(jù)塊 {done, value} │ │ │ └── 數(shù)據(jù)塊通常是 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> | 二進制數(shù)據(jù)、文件解析 |
response.formData() | Promise<FormData> | 表單響應(少見) |
response.body | ReadableStream | 實時處理、進度跟蹤 |
注意:
fetch 的響應體只能被讀取一次 (即:不能同時調(diào)用 response.json()
和 response.text()
等 )
即使響應有錯誤狀態(tài)(如 404),fetch
不會拋出異常,需要手動檢查 response.ok
。如下代碼處理方式:
let response = await fetch('https://example.com'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
完整代碼見:github
到此這篇關于vue3實現(xiàn)ai聊天對話框的文章就介紹到這了,更多相關vue3 ai聊天對話框內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
使用axios請求接口,幾種content-type的區(qū)別詳解
今天小編就為大家分享一篇使用axios請求接口,幾種content-type的區(qū)別詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-10-10elementUI?checkBox報錯Cannot read property &ap
這篇文章主要為大家介紹了elementUI?checkBox報錯Cannot read property 'length' of undefined的解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06AntV F2和vue-cli構(gòu)建移動端可視化視圖過程詳解
這篇文章主要介紹了AntV F2和vue-cli構(gòu)建移動端可視化視圖過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-10-10