hyosan-chat 是一个基于 Lit 和 Shoelace 实现的 AI 对话组件库, 基于 Web Components 技术栈, 适用于任何框架(vue / react / angular / vanilla ...), 该项目旨在提供一个现代化、高性能且易于扩展的 Web 组件库, 用于构建智能对话界面; 最终实现效果将类似于 ant-design-x
Web Components: 使用 Lit 构建的自定义元素, 确保跨框架兼容性, 目前已在 vue / react / angular 等框架中测试通过UI组件库: 基于成熟的基础组件库 ShoelaceAI集成: 支持与多种AI模型和服务集成, 提供智能对话功能- 模块化设计: 组件高度解耦, 便于按需引入和扩展
- 性能优化: 通过 vite@^6.1.0 构建工具链, 确保快速开发和高效的生产环境性能
- Lit@^3.2.1:
Web Component库 - shoelace@^2.20.0: 使用
Web Components实现的UI组件库 - vite@^6.1.0: 现代化的前端构建工具
- TypeScript: 强类型语言, 确保代码质量和可维护性
- biome:代码格式化和
lint工具, 保证代码风格一致性和质量
pnpm i hyosan-chat🔗 demo 页面 的源码可直接参考 src/hyosan-chat-demo.ts
组件附带了一个用于声明自定义元素信息的文件, 可以实现在 vscode / JetBrains IDE 中的代码补全功能
需要在 vscode 的 settings.json 中声明组件提供的 types:
.vscode/settings.json:
{
+ "html.customData": [
+ "./node_modules/hyosan-chat/dist/cem-types/vscode.html-custom-data.json"
+ ]
}组件已经声明了一个 dist/web-types.json 文件, 在 JetBrains IDE 中应该会检测到, 如果没有任何提示, 你可能需要在 package.json 中声明 web-types, 可参考 JetBrains IDEs - Shoelace
组件完全使用 TypeScript 编写, 也基于 custom-elements-manifest 提供了一流的 TypeScript 支持, 只需引入组件提供的类型文件即可
tsconfig.json:
{
"compilerOptions": {
// ...
+ "types": [
+ "hyosan-chat/dist/cem-types/vue/index.d.ts"
+ ]
},
}
Tip
具体使用方式可参考示例项目: hyosan-chat-vue-demo
请先阅读官方文档 在 Vue 中使用自定义元素
在 Vue 中默认将所有元素作为 vue 组件, 但自定义元素不能被当做 vue 组件进行处理, 我们需要显示地声明哪些是自定义组件:
vite.config.ts:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: tag => tag.includes('hyosan-')
}
}
})
],
})Tip
对于 vue2 项目, 应该配置 Vue.config.ignoredElements
以上配置是将 hyosan-* 组件作为自定义组件处理
- 在代码中我们也必须严格使用
<hyosan-chat>, 而不能写成<HyosanChat> - 对于
slot也不能使用v-slot/#语法, 因为Web Components的插槽是原生的 slot, vue 的特殊插槽语法无法在自定义组件中使用
Tip
关于自定义元素使用插槽的局限性可参考 插槽 - vue
vue 对于 Property 参数(在 Properties 中标注了哪些属性是 Property) 必须添加 .prop 修饰符:
<hyosan-chat :messages.prop="messages"></hyosan-chat>Tip
直接参考示例项目: hyosan-chat-react-demo
组件库使用 Lit 搭建, 并通过 custom-element-react-wrappers 使组件库使用 forwardRef 进行了包装, 以便为 react 项目提供组件和类型支持, 你可以直接参考上方的示例项目, 也可参考 #ad39286 了解具体的改动
需要特别注意的是, react 组件本身不是 HTML 元素, 无法直接作为 slot, 应该包裹一个具有 slot 属性的容器元素, 然后在容器元素中放置组件, 详见 using slot - Lit
如果你想了解更多关于自定义组件如何在 react 项目中使用的细节, 可参考以下文档:
Tip
直接参考示例项目: hyosan-chat-angular-demo
具体改动可参考 #71a4ddf
Tip
直接参考示例项目: hyosan-chat-vanilla-demo
具体改动可参考 #c12b2b7
vanilla 指的是原生的没有使用任何框架的 javascript 运行环境或项目, 可以理解为完全空白的项目
Tip
关于 属性类型:
Lit@^3.2.1 组件可以接收 Property / Attribute 参数:
Attribute: 通过HTML元素的属性(attribute) 传递数据, 并且attribute属性值会转换为string类型Property: 通过JS获取组件, 并 在组件对象上 添加属性, 并且Property属性值会转换为JS原生数据类型
| 属性名 | 类型 | 属性类型 | 默认值 | 描述 | Reflect |
|---|---|---|---|---|---|
applicationTitle |
string |
Attribute |
'Hyosan Chat' |
应用标题 | |
panelSnap |
string |
Attribute |
'25% 50%' |
分割面板的可捕捉位置 | ✅ |
panelPosition |
number |
Attribute |
25 |
分隔线与主面板边缘的当前位置(百分比, 0-100), 默认为容器初始大小的 50% |
✅ |
💡 conversations |
Array<Conversation> |
Property |
[] |
会话列表数据源 | ✅ |
currentConversationId |
BaseService |
Attribute |
'' |
当前会话 ID | ✅ |
💡 service |
BaseService |
Property |
new DefaultService() |
会话服务配置参数 | |
💡 messages |
BaseServiceMessages |
Property |
undefined |
会话服务消息列表 | ✅ |
showAvatar |
boolean |
Attribute |
true |
是否显示头像 | ✅ |
showRetryButton |
boolean |
Attribute |
true |
是否显示 重新生成 按钮 | |
showLikeAndDislikeButton |
boolean |
Attribute |
true |
是否显示 👍 和 👎 按钮 | |
💡 onCreateMessage |
(content?: string) => string | Promise<string> |
Property |
undefined |
创建消息的回调函数, 当 没有选中会话 或 点击开始新聊天按钮 时, 如果直接开始发送消息, 会调用此函数, 此回调函数中应该创建新的 conversation 并更新 messages, 组件会等待函数返回一个 conversationId, 然后再发送消息; 如果不返回 conversationId, 则不会在组件内部改变 conversationId, 这就相当于创建了一个没有回话 ID 的临时聊天 |
|
onEnableSearch |
(open: boolean) => void | Promise<void> |
Property |
undefined |
如果传入则显示联网搜索按钮, 用户点击搜索按钮时 调用此方法 | |
shoelaceTheme |
HyosanChatShoelaceTheme |
Attribute |
HyosanChatShoelaceTheme.shoelaceLight |
shoelace 主题, 可用于切换夜间模式 | |
avatarGetter(0.3.1) |
(message: BaseServiceMessageItem) => TemplateResult |
Property |
undefined |
消息列表中的头像获取函数, 传入则显示此函数的返回值, 返回值必须是 html<div>...</div> 格式的 html, 详见 lit html slot |
|
onBeforeSendMessage(0.3.2) |
(service: BaseService, messages: BaseServiceMessages) => void | Promise<void> |
Property |
undefined |
在每次发送消息之前执行 | |
showReadAloudButton(0.4.0) |
boolean |
Attribute |
false |
是否显示 朗读 按钮 | |
onSendFirstMessage(0.4.1) |
Promise<number | string | undefined> | number | string | undefined |
Property |
undefined |
在当前会话中首次发送 user 消息时调用, 一般用于更新当前会话的 label; 返回一个 number | string 值, 将作为消息内容(content)或其最大截取长度(返回 number 时)并赋值给 label 或 直接作为 label |
|
onMessagePartsRender(0.5.0) |
(part: HyosanChatMessageContentPart, message: BaseServiceMessageItem) => Promise<boolean> |
Property |
undefined |
消息部分渲染函数, 返回 true 则跳过组件内部的处理逻辑 |
|
onAfterMessagePartsRender(0.5.0) |
(part: HyosanChatMessageContentPart, message: BaseServiceMessageItem) => Promise<void> |
Property |
undefined |
消息部分渲染函数(after) |
|
uploadHandler(0.6.0) |
HyosanChatUploadHandler |
Property |
false |
上传附件的处理对象, 若值为空, 则不启用上传附件功能 |
[!TIP] 关于 插槽 Lit@^3.2.1 的插槽与
vue的插槽不同, 基于原生的<slot>元素 实现, 不具备作用域插槽, 也不能在组件内部多次渲染插槽
| 名称 | 描述 |
|---|---|
conversations |
左侧会话列表 |
conversations-header |
左侧会话列表的 header 部分 |
conversations-footer |
左侧会话列表的 footer 部分 |
main-welcome |
右侧消息列表的 welcome 界面 |
main-header |
右侧消息列表的 header 部分 |
settings-main-header |
设置弹窗(从顶部的设置按钮打开)中的表单项部分 |
settings-main-aside |
设置弹窗(从侧边栏底部的设置按钮打开)中的表单项部分 |
其中 settings-main-header 和 settings-main-aside 都是在设置弹窗中显示的内容, 但因为 slot 不能多次渲染, 为了避免渲染失败, 所以将其分为两个 slot, 在使用时应该入相同的内容
由于原生的 <slot> 元素 存在诸多限制, 既无法在组件内部渲染多次, 也无法实现作用域插槽, 所以本组件对外 export 了 html - lit 方法, 用于创建在 lit 中使用的 html 模板:
import { html } from 'hyosan-chat'
const avatar = html`<div>Hello Lit html</div>`html 的语法可参考 lit html / Rendering - Lit
| 事件名 | 参数 | 描述 |
|---|---|---|
conversations-create |
undefined |
点击创建新会话按钮 |
click-conversation |
CustomEvent<{ item: Conversation }> |
点击左侧会话列表中的会话 |
change-conversation |
CustomEvent<{ item: Conversation }> |
点击 切换 左侧会话列表中的会话 |
send-message |
CustomEvent<{ content: string }> |
点击发送按钮 |
hyosan-chat-settings-save |
CustomEvent<{ settings: ChatSettings }> |
在设置弹窗中点击保存按钮 |
edit-conversation |
CustomEvent<{ item: Converastion }> |
在会话列表中点击编辑按钮, 并保存 |
delete-conversation |
CustomEvent<{ item: Converastion }> |
在会话列表中点击删除按钮 |
hyosan-chat-click-like-button |
CustomEvent<{ message: BaseServiceMessageItem }> |
点击 Like 按钮(点赞) |
hyosan-chat-click-dislike-button |
CustomEvent<{ message: BaseServiceMessageItem }> |
点击 Dislike 按钮(点踩) |
first-updated |
CustomEvent<{ service: BaseService }> |
lit 原生的 first-updated hooks 触发时执行 |
messages-completions |
CustomEvent<{ messages: BaseServiceMessages }> |
消息接收完毕(可能是成功或报错) |
first-updated-complete |
CustomEvent<{ service: BaseService }> |
lit 原生的 first-updated hooks 触发后等待 updateComplete 后执行 |
localize-update-conversations(0.4.1) |
CustomEvent<{ conversations: Array<Conversation> }> |
当启用本地存储时, 组件首次加载时获取 conversations 数据时触发 |
可以使用 ::part() 选择器修改组件的样式, 由于 Web Components 的样式隔离的特性, 组件外部想要修改组件内的样式只能通过 ::part() 选择器或组件内部引用的 css 变量 来进行控制
| 名称 | 描述 |
|---|---|
base |
根组件(hyosan-chat) 最外层元素 |
组件提供的 css 变量包含两部分:
- 基础组件库 shoelace 的 css 变量: 参考 Themes - shoelace
- 组件内部使用的 css 变量: 参考 src/sheets/global-styles.css 文件
组件通过底层的基础组件库 shoelace 提供了基础的 light / dark 两种主题, 如需创建新主题, 可参考 Creating a theme
BaseService 是组件在请求和处理聊天消息时的抽象类, 它将处理聊天消息的逻辑进行了抽象, 在实际项目中可以根据情况实现自己的 Service, 组件默认使用 DefaultService, 它继承了 BaseService 抽象类, 并且实现了 BaseService 的抽象方法, 提供了默认的消息处理逻辑
现阶段从用户发起聊天到聊天内容渲染到 DOM 元素上, 消息内容的处理 都是由 Service 完成的, 以聊天界面为例:
- 用户在输入框内输入内容, 点击发送按钮或按下
Enter键,hyosan-chat组件会执行_handleSendMessage方法 - 更新
Service上的聊天配置参数, 并通过this.service.emitter监听Service提供的事件 - 如果会话不存在, 则调用
onCreateMessageproperty来创建会话和messages - 触发
onBeforeSendMessageproperty回调函数 - 调用
this.service.send()/this.service.retry()方法来发送或重新发送消息 - (
DefaultService) 在send()中处理messages, 如果不存在system message则加入包含默认系统提示词(this.service.systemPrompt) 的system message, 并加入用户消息内容 - (
DefaultService) 调用setChatCompletionParams()设置聊天流式请求接口的相关参数 - (
DefaultService)this.emitter.emit('before-send')触发before-send事件 - (
DefaultService) 调用this.fetchChatCompletion()触发before-send事件 - (
DefaultService) 创建this.abortController用于停止流式请求 - (
BaseService) 调用this.getEmptyAssistantMessage()加入助手消息 - (
BaseService) 调用this.handleRequestMessages()将messages处理为请求参数messages - (
DefaultService) 发起流式请求并触发相关事件: send-open: 已建立连接data: 接收流式请求返回数据send-done: 所有内容返回完毕error: 消息请求报错abort: 中断连接close: 关闭连接- 进入
finally代码块 this.service.emitter.clearListeners()移除Service上的所有事件监听器- 将所有新消息的
$loading设置为false - 触发
messages-completions事件
其中 DefaultService / BaseService 就是在 Service 上执行的, 以上步骤可简化为:
- 组件监听到用户发送消息, 调用
this.service.send() - 在
DefaultService内部处理请求参数并发起请求 - 在请求开始直到请求结束时触发指定的事件
- 请求结束后在组件中触发
messages-completions事件
Tip
<hyosan-chat> 组件的 service 是一个 property, 如果要自定义消息数据的处理逻辑, 可以直接创建一个新的 Service 并实现 BaseService 抽象类, 然后将新的 Service 传给 <hyosan-chat> 组件
在 Service 中发起了聊天请求, 并不断地更新 messages, 但接口返回的消息内容为 markdown 格式, 最终渲染到页面上时需要将 markdown 转换为 html, 下面介绍转换的步骤:
- 在
Service中的每次流式请求返回内容时, 更新messages中的消息内容 - 在
<hyosan-chat-bubble-list>组件中, 监听到messages变化时, 调用markdown-it-async异步地将markdown转换为html string - 在异步转换结束后, 更新
this.messagesHtml, 调用this.requestUpdate()触发 DOM 层渲染(render()) - 在
render()中根据this.messagesHtml渲染出消息内容DOM(使用innerHTML)
参考 CONTRIBUTING




