122
社区成员
发帖
与我相关
我的任务
分享ChatUI是阿里巴巴开源的一套服务于对话领域的设计和开发体系,助力智能对话机器人的搭建。
特性如下:
SSE(Server-Sent Events)是一种用于实现服务器主动向客户端推送数据的技术,也被称为“事件流”(Event Stream)。它基于 HTTP 协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端的实时数据推送。
Chat UI文档:https://chatui.io/sdk/getting-started
SSE API: https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource

如图所示,本项目使用了ChatUI + SSE实现了AI辅助创作的功能,核心代码框架如下:

当用户通过用户界面输入发送文本消息的时候,会触发send函数,send函数返回一个Promise,在这个Promise的状态确定之前,ChatUI会呈现出滚动的句号等待状态。

在Promise中,我们通过fetchEventSource(封装了原生SSE操作),向我们的AI后端发送请求,建立SSE连接。
连接建立成功后,我们会收到SSE事件源发送的流式响应,当收到第一条消息时,我们就可以resolve当前这个Promise了,此时Chat UI就会停止当前的等待状态,我们开始处理流式消息的拼接。
具体拼接的实现:
由于SSE返回的数据是一段一段的,我们通过DOM操作拿到当前最后一个回复的消息元素,在每次onmessage出发的时候,给这个DOM的innerHTML属性重新赋值,并且讲滚动条设置到最底部,在视觉上就会呈现出文字不断出现的拼接效果了。
onmessage: (event) => {
// ctx.scrollToEnd()
let jsonData = JSON.parse(event.data)
if (jsonData.data) {
resStr += jsonData.data
}
if (firstReceive) {
resolve(' ')
firstReceive = false
return
}
// resolve(resStr)
if (interval) return
interval = setInterval(() => {
let lastMsg = Array.from(
document.querySelectorAll('.Knowledge-content'),
).pop()
console.log('lastMsg', lastMsg)
lastMsg.innerHTML = resStr
var div = document.querySelector('.PullToRefresh')
div.scrollTop = div.scrollHeight - div.clientHeight
clearInterval(interval)
interval = null
}, SPEED)
},
ChatUI的文档十分简单,没有提供一个流式问答的完整案例,我们项目需要的效果需要较高的定制性,缺乏详细文档,实现流式问答困难
解决:
好在项目是开源的,直接阅读相关源码,结合Github Issue,来找到答案。(文档有时候不准确,代码永远不会骗你)

接口响应速度太快,导致onmessage频繁触发,频繁重新赋值,导致DOM渲染卡顿,视觉上出现空白的状态
解决:
我们加入一个定时器,来控制重新赋值的速度
if (interval) return
interval = setInterval(() => {
let lastMsg = Array.from(
document.querySelectorAll('.Knowledge-content'),
).pop()
console.log('lastMsg', lastMsg)
lastMsg.innerHTML = resStr
var div = document.querySelector('.PullToRefresh')
div.scrollTop = div.scrollHeight - div.clientHeight
clearInterval(interval)
interval = null
}, SPEED)
如何与编辑器联动,实现选中提问,提问后一键引入到编辑器?
解决:
由于组件化的设计,我们需要在redux仓库中定义一个公共的状态selectedText,通过编写编辑器插件实现选中扩写、改写事件的触发,再在触发的事件中使用ChatUI声明式的发送消息

以扩写为例的核心代码:
/* 监听是否有选中的内容(点击扩写会改变) */
useEffect(() => {
if (selectedText) {
expendFlag = true
ctx.postMessage({
type: 'text',
content: {
text: selectedText,
},
position: 'right',
})
}
}, [selectedText])
在根据ChatUI Pro提供的自定义卡片功能,实现一个特定回复类型卡片组件并引入,便可以完成导入编辑器
/* 生成了需要添加的内容后,将其插入卡片 */
useEffect(() => {
if (!contentToAdd) return
ctx.appendMessage({
type: 'card',
content: {
code: 'adaptable-action-card', // 卡片code
data: {
title: selectedText.slice(0, 2) + '结果',
content: contentToAdd,
actionList: [
{
text: '应用',
action: '',
style: 'default',
param: {
/* url: 'https://www.taobao.com', */
},
},
{
text: '重新生成',
action: 'sendText',
style: 'default',
param: {
text: '重新生成中',
},
},
],
}, // 卡片数据
},
})
}, [contentToAdd])
绑定应用按钮的点击事件,完成引入功能
/* 事件委托,绑定点击应用的事件 */
useEffect(() => {
const messageListDOM = document.querySelector('.MessageList')
console.log('messageListDOM', messageListDOM)
// 定义事件处理函数
function handleClick(event) {
if (
event.target.classList.contains('Btn') &&
event.target.textContent == '应用'
) {
console.log('dispatch setEditorAddedContent', contentToAdd)
/* 派发,告诉Editor添加 */
dispatch(setEditorAddedContent({ contentToAdd }))
}
}
// 在父元素上添加事件监听器,实现事件委托
messageListDOM.addEventListener('click', handleClick)
// 返回一个清理函数,在组件销毁时移除事件监听器
return () => {
messageListDOM.removeEventListener('click', handleClick)
}
}, [contentToAdd])
最后还是顺利地用ChatUI和SSE完成了这个AI辅助创作的效果,当初使用Chat UI也是因为没有时间从头到尾再实现一个自己的Chat界面,还要支持大量的事件和组件,初看他的文档感觉功能应有尽有,可以满足我们项目的需求,但是实现的时候发现还是存在很多问题,好在最后都一一解决了。
刚刚点进官网发现Chat UI Pro又升级成了Chat SDK,可以直接接入阿里云智能客服,只需要通过简单的配置就能搭建出对话机器人;同时它强大易扩展,通过丰富的接口和自定义卡片满足各种定制化需求。
(为什么不早点出来。。。