hyosan-chat
v0.6.0
Published
A library of web components for AI conversations based on lit and shoelace
Maintainers
Readme
hyosan-chat

介绍
hyosan-chat 是一个基于 Lit 和 Shoelace 实现的 AI 对话组件库, 基于 Web Components 技术栈, 适用于任何框架(vue / react / angular / vanilla ...), 该项目旨在提供一个现代化、高性能且易于扩展的 Web 组件库, 用于构建智能对话界面; 最终实现效果将类似于 ant-design-x
Demo
功能特性
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
需要在 vscode 的 settings.json 中声明组件提供的 types:
.vscode/settings.json:
{
+ "html.customData": [
+ "./node_modules/hyosan-chat/dist/cem-types/vscode.html-custom-data.json"
+ ]
}JetBrains IDE
组件已经声明了一个 dist/web-types.json 文件, 在 JetBrains IDE 中应该会检测到, 如果没有任何提示, 你可能需要在 package.json 中声明 web-types, 可参考 JetBrains IDEs - Shoelace
TypeScript
组件完全使用 TypeScript 编写, 也基于 custom-elements-manifest 提供了一流的 TypeScript 支持, 只需引入组件提供的类型文件即可
tsconfig.json:
{
"compilerOptions": {
// ...
+ "types": [
+ "hyosan-chat/dist/cem-types/vue/index.d.ts"
+ ]
},
}
vue
[!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>react
[!TIP] 直接参考示例项目: hyosan-chat-react-demo
组件库使用 Lit 搭建, 并通过 custom-element-react-wrappers 使组件库使用 forwardRef 进行了包装, 以便为 react 项目提供组件和类型支持, 你可以直接参考上方的示例项目, 也可参考 #ad39286 了解具体的改动
需要特别注意的是, react 组件本身不是 HTML 元素, 无法直接作为 slot, 应该包裹一个具有 slot 属性的容器元素, 然后在容器元素中放置组件, 详见 using slot - Lit
如果你想了解更多关于自定义组件如何在 react 项目中使用的细节, 可参考以下文档:
angular
[!TIP] 直接参考示例项目: hyosan-chat-angular-demo
具体改动可参考 #71a4ddf
vanilla
[!TIP] 直接参考示例项目: hyosan-chat-vanilla-demo
具体改动可参考 #c12b2b7
vanilla 指的是原生的没有使用任何框架的 javascript 运行环境或项目, 可以理解为完全空白的项目
API
Properties
[!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 | 上传附件的处理对象, 若值为空, 则不启用上传附件功能 | |
Slots
[!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, 在使用时应该入相同的内容
Lit html 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
Events
| 事件名 | 参数 | 描述 |
| --- | --- | --- |
| 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 数据时触发 |
CSS Parts
可以使用 ::part() 选择器修改组件的样式, 由于 Web Components 的样式隔离的特性, 组件外部想要修改组件内的样式只能通过 ::part() 选择器或组件内部引用的 css 变量 来进行控制
| 名称 | 描述 |
| --- | --- |
| base | 根组件(hyosan-chat) 最外层元素 |
CSS Variables
组件提供的 css 变量包含两部分:
- 基础组件库 shoelace 的 css 变量: 参考 Themes - shoelace
- 组件内部使用的 css 变量: 参考 src/sheets/global-styles.css 文件
主题
组件通过底层的基础组件库 shoelace 提供了基础的 light / dark 两种主题, 如需创建新主题, 可参考 Creating a theme
Service
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
