在小程序中實現(xiàn)ChatGPT?聊天打字兼自動滾動效果
一 前言
ChatGPT 已經(jīng)長時間大火,未來將會是AI的天下。人們需要更多地學(xué)習(xí)和掌握AI,而不是被AI所取代。
目前市面上已經(jīng)有很多類似 chatGPT 的智能應(yīng)用,應(yīng)用有可能是 web h5 應(yīng)用,也有可能是小程序或者是 Native 應(yīng)用。隨著 ChatGPT 深入,移動端也會再次火爆起來。
在 ChatGPT 的背景下,我們今天來聊聊在小程序中怎么實現(xiàn)類似 chatGPT 的聊天打字效果,并且實現(xiàn)滾動效果,具體如下:
這篇文章將深入一下內(nèi)容:
- 小程序怎么樣實現(xiàn)動態(tài)打字效果。
- 怎么實現(xiàn)隨打字效果滾動。
- 請求分片知識點(diǎn)。
- scroll-view 細(xì)節(jié)處理等。
二 實現(xiàn)打字效果
2.1.預(yù)熱內(nèi)容—數(shù)據(jù)請求與接收
開發(fā)者可以接入 openAi 提供的接口,實現(xiàn)自定義的問答流程。在聊天會話中,我們問 chatGPT 一句話:
介紹一下跨端開發(fā)
那么和平常的請求不同的是,數(shù)據(jù)并不是一次性返回的,而是采用 stream 流式返回的。我們可以在 Network 中看到 response 的大體結(jié)構(gòu):
如上可以看到返回的 text 是分片處理的,每次會返回一小段內(nèi)容,只要前端根據(jù)返回這一小段內(nèi)容就可以了,也就自然形成了打字的效果。
可能會有同學(xué)好奇,這種分片的數(shù)據(jù)結(jié)構(gòu),前端應(yīng)該怎么接收呢?實際很簡單,我們拿 axios 為例子,開發(fā)者可以通過監(jiān)聽onDownloadProgress 事件來接受服務(wù)端返回的文本片段。具體例子如下:
axios({ method: 'post', url: 'https:xxx.xxx, onDownloadProgress: function({ event }) { const xhr = event.target const { responseText } = xhr /* 獲取返回的內(nèi)容,本質(zhì)上是 json 字符串 */ let chunk = responseText try{ /* 序列化返回的內(nèi)容 */ const data = JSON.parse(chunk) /* chatGPT 返回的內(nèi)容 */ console.log(data.text) }catch(e){ } } })
這里描述請求的流程,通過 onDownloadProgress 來監(jiān)聽返回的內(nèi)容,然后獲取到返回的內(nèi)容,JSON.parse
解析內(nèi)容,這里有一個注意事項,就是對于 JSON.parse
應(yīng)該加上 try catch ,防止解析的失敗。
2.2.小程序中接口處理
小程序沒有如上 axios 里面監(jiān)聽 stream 流式響應(yīng)數(shù)據(jù)的能力,也沒有處理 onDownloadProgress 的回調(diào)函數(shù)。簡單來說 onDownloadProgress 的實現(xiàn),本質(zhì)上是 axios 在瀏覽器發(fā)起 http 請求,會創(chuàng)建一個 XHR 對象,其用于發(fā)送請求和接收響應(yīng),在創(chuàng)建 XHR 對象后,axios 會注冊一個 progress 事件監(jiān)聽器到 XHR 對象上,用于獲取下載的進(jìn)度信息。
那么小程序中如何實現(xiàn)分片流式下載呢?在小程序中,統(tǒng)一收口到 request 中,在 request 中可以用 RequestTask 的 onChunkReceived 來接收服務(wù)端的分片數(shù)據(jù)。這個方法可以監(jiān)聽 Transfer-Encoding Chunk Received 事件。當(dāng)接收到新的 chunk 時觸發(fā)。
我們來看看具體怎么使用:
const requestTask = wx.request({ enableChunked:true, // 開啟分片模式 ... }) requestTask.onChunkReceived((res)=>{ // 接收分片的數(shù)據(jù) })
這樣就可以通過分片來實現(xiàn)打字的效果。
2.3.打字效果實現(xiàn)
接下來我們看一下小程序是如何實現(xiàn)打字效果的,先不考慮返回的數(shù)據(jù)是 stream 流式結(jié)構(gòu),先認(rèn)為返回的數(shù)據(jù)格式是整個文本,那么應(yīng)該怎么樣處理文本呢。
首先我們聊天的內(nèi)容如下所示:
如上, 每一個消息都是一個 message-item ,所有的 message 保存到了 messageList 列表中,在 wxml 中如下所示:
<view wx:for="{{messageList}}" wx:key="id" id="item-{{item.id}}"> <message-item data-index="{{index}}" role="{{item.role}}" content="{{item.content}}" finished="{{item.finished}}" bind:share="handleMessageShare" /> </view>
如上,可以看到 message-item 保存了一條會話內(nèi)容。
當(dāng)我們發(fā)一條信息的時候,產(chǎn)生一條 message-item 。接下來 chatGPT 返回內(nèi)容后,也會產(chǎn)生一條 message-item ,要實現(xiàn)打字效果就是這條 message-item 。
我們只需要將這條 message-item 的內(nèi)容,通過 setData 方式分片渲染就可以了。比如我們想打字實現(xiàn) ‘您好GPT’,那么分五次 setData 渲染就可以了,比如如下:
您
您好
您好G
您好GP
您好GPT
如上就是分五次渲染,每一次渲染的結(jié)果。接下來就是代碼的實現(xiàn)。
this.handleRequestResolve(data.text)
比如 chatGPT 每次返回一條內(nèi)容,都用 handleRequestResolve 函數(shù)處理返回的內(nèi)容??匆幌?handleRequestResolve 的核心實現(xiàn)。
handleRequestResolve(result){ const timestamp = Date.now(); const index = this.data.messageList.length const newMessageList = `messageList[${index}]` const contentCharArr = result.trim().split("") const content_key = `messageList[${index}].content` const finished_key = `messageList[${index}].finished` this.setData({ thinking: false, [newMessageList]: { id: timestamp, role: 'assistant', finished: false } }) currentContent = '' this.showText(0, content_key, finished_key, contentCharArr); }
在 handleRequestResolve 中會構(gòu)建一條新的 message-item ,然后就是 showText
展示內(nèi)容,來看一下 showText 怎么處理內(nèi)容。
showText(key = 0, content_key, finished_key, value) { /* 所有內(nèi)容展示完成 */ if (key >= value.length) { this.setData({ loading: false, [finished_key]: true }) wx.vibrateShort() return; } currentContent = currentContent + value[key] /* 渲染回話內(nèi)容 */ this.setData({ [content_key]: currentContent, }) setTimeout(() => { /* 遞歸渲染內(nèi)容 */ this.showText(key + 1, content_key, finished_key, value); }, 50); },
這樣用遞歸就實現(xiàn)了打字效果。我們來看一下效果:
通過上面可以看到,在文字打印的過程中,列表不能跟隨一起滾動,當(dāng)文字內(nèi)容超出一屏幕之后,視圖就停止了(本質(zhì)上數(shù)據(jù)在后面追加),這是一個很不好的效果。
接下來,我們進(jìn)行優(yōu)化處理,讓視圖可以根據(jù)內(nèi)容自動滾動。
三 如何實現(xiàn)視圖跟隨內(nèi)容滾動
3.1 實現(xiàn)原理
實現(xiàn)視圖跟隨內(nèi)容滾動實際很簡單,因為 message-item 的容器本質(zhì)上就是一個 scroll-view , 那么想要 scroll-view 視圖跟隨返回內(nèi)容變化,只需要動態(tài)設(shè)置 scroll-view 的 scroll-top 值就可以了。
視圖跟隨內(nèi)容滾動,本質(zhì)上就是讓 scroll-view 一直自動滾動到底部, 如何要讓 scroll-view 一直滾動到底部呢?先看一下如下示意圖:
如上可以看到,想讓 scroll-view 一直滾動到底部,只需要讓 scroll-top 等于 scroll-view 內(nèi)容高度減去 scroll-view 容器本身高度就可以了。
所以需要我們給 scroll-view 里面的內(nèi)容,用一個 view 包裹如下:
如上 scroll-view 的類名為 content
, scroll-view 內(nèi)部元素的類名為 scroll-view-content
,接下來可以通過如下代碼設(shè)置 scroll-top 值了。
handleScollTop() { return new Promise((resolve) => { const query = wx.createSelectorQuery() query.select('.content').boundingClientRect() query.select('.scroll-view-content').boundingClientRect() query.exec((res) => { const scrollViewHeight = res[0].height const scrollContentHeight = res[1].height if (scrollContentHeight > scrollViewHeight) { const scrollTop = scrollContentHeight - scrollViewHeight this.setData({ scrollTop }, () => { resolve() }) }else{ resolve() } }) }) },
如上通過 createSelectorQuery
分別獲取 scroll-view 和 scroll-view 內(nèi)部元素的高度,兩者的差值就是 scroll-top 值。
接下里在渲染會話內(nèi)容的時候,渲染之后,調(diào)用 handleScollTop 來動態(tài)設(shè)置 scroll-top 就可以了。
showText(key = 0, content_key, finished_key, value) { if (key >= value.length) { this.setData({ loading: false, [finished_key]: true }) wx.vibrateShort() return; } currentContent = currentContent + value[key] this.setData({ [content_key]: currentContent, },()=>{ this.handleScollTop().then(()=>{ setTimeout(() => { this.showText(key + 1, content_key, finished_key, value); }, 20); }) }) },
這里有一個小細(xì)節(jié),就是在渲染上一次文本內(nèi)容之后,需要先校驗一下 scroll-top 值,然后再次調(diào)用 showText 來渲染會話內(nèi)容。
我們來看一下效果。
后續(xù)優(yōu)化: 本質(zhì)上不需要在每次 showText 之后都通過 createSelectorQuery 異步獲取元素 scroll-top 并再次渲染,這無疑是性能的浪費(fèi),實際可以控制 createSelectorQuery 到 setData 設(shè)置 scroll-top 值的頻率來提升性能。
四 總結(jié)
感興趣的同學(xué)可以自己實現(xiàn)一個會話打字效果,其中還有很多小細(xì)節(jié)這里就不講了。
以上就是小程序?qū)崿F(xiàn)ChatGPT 聊天打字兼自動滾動效果的詳細(xì)內(nèi)容,更多關(guān)于ChatGPT聊天自動滾動的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
thymeleaf實現(xiàn)th:each雙重多重嵌套功能
今天給大家分享一個使用 thymeleaf 實現(xiàn)一個動態(tài)加載一二級文章分類的功能,本文通過代碼講解的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友參考下吧2019-11-11Scala項目構(gòu)建工具sbt和IntelliJ IDEA環(huán)境配置詳解
這篇文章主要介紹了Scala項目構(gòu)建工具sbt和IntelliJ IDEA環(huán)境配置,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10一文詳解kafka開啟kerberos認(rèn)證的完整步驟
這篇文章主要為大家詳細(xì)介紹了kafka開啟kerberos認(rèn)證的完整步驟,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03win10環(huán)境安裝kettle與linux環(huán)境安裝kettle的詳細(xì)過程
kettle是一款免費(fèi)開源的、可視化的、國際上比較流行的、功能強(qiáng)大的ETL必備工具,在ETL這一方面做的還不錯,下面介紹一下基于win10操作系統(tǒng)安裝kettle和linux操作系統(tǒng)安裝kettle的詳細(xì)過程,感興趣的朋友跟隨小編一起看看吧2022-11-1110分鐘搞定讓你困惑的 Jenkins 環(huán)境變量過程詳解
這篇文章主要介紹了10分鐘搞定讓你困惑的 Jenkins 環(huán)境變量過程詳解,本文通過圖文實例相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01antd通過 filterDropdown 自定義按某天時間搜索功能
這篇文章主要介紹了antd通過 filterDropdown 自定義按某天時間搜索功能,本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-08-08