用 TinyRobot Bubble 组件打造灵活强大的 AI 对话气泡
引言:为什么 AI 对话界面需要灵活的气泡组件?
在 AI 对话应用开发中,消息气泡远不止是一个"框里放文字"的简单 UI。随着大语言模型能力的演进,AI 回复可能包含流式输出文本、Markdown 格式、代码块、图片、工具调用结果、推理过程 等多种异构内容。同时,对话界面还需要处理消息分组、角色区分、加载状态、交互式内容等复杂需求。
如果每次都要从零实现这些能力,开发者将陷入无尽的 UI 适配和状态管理泥潭。TinyRobot Bubble 组件专为解决这些痛点而设计------它提供了一套渲染器架构的声明式气泡系统,让你用极少的代码构建出专业级的 AI 对话界面。
核心能力一览
| 特性 | 说明 |
|---|---|
| 多类型消息展示 | 文本、图片、Markdown、代码等 |
| 流式输出 | 响应式 content,天然支持打字机效果 |
| 消息分组 | 连续/分割/自定义三种分组策略 |
| 渲染器架构 | Box 渲染器 + Content 渲染器,完全可扩展 |
| 状态管理 | 内置 state + state-change 事件 |
| 丰富插槽 | prefix、suffix、after、content-footer |
| CSS 变量 | 50+ 变量覆盖气泡各子模块样式 |
代码示例
1. 基础气泡使用
最简单的气泡只需要一个 content 属性。通过 CSS 变量可以快速调整样式。
vue
<template>
<tr-bubble
content="TinyVue 是一个轻量级、高性能的 Vue 3 组件库,专为企业级应用设计。"
style="--tr-bubble-box-bg: var(--tr-color-primary-light); --tr-bubble-text-font-size: 16px"
/>
</template>
<script setup lang="ts">
import { TrBubble } from '@opentiny/tiny-robot'
</script>
TrBubble 是气泡组件的基础入口。传入字符串作为 content,组件会自动使用内置的 BubbleRenderers.Text 渲染器展示文字。通过 --tr-bubble-box-bg 可以设置气泡背景色,--tr-bubble-text-font-size 控制文字大小。
2. 流式文本------AI 回复的打字机效果
AI 回复通常是逐字生成的。Bubble 的 content 是响应式的------动态追加内容即可实现流式输出。
vue
<template>
<div style="display: flex; flex-direction: column; gap: 16px">
<button @click="startStream">开始流式输出</button>
<tr-bubble :content="streamContent" :avatar="aiAvatar" />
</div>
</template>
<script setup lang="ts">
import { TrBubble } from '@opentiny/tiny-robot'
import { IconAi } from '@opentiny/tiny-robot-svgs'
import { h, ref } from 'vue'
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
const fullText = '二进制中 1+1 的结果是 10。在二进制系统中,1+1 产生进位,结果为 0 并进位 1。'
const streamContent = ref('')
const startStream = async () => {
streamContent.value = ''
for (const char of fullText) {
streamContent.value += char
await new Promise((resolve) => setTimeout(resolve, 80))
}
}
</script>
关键点:content 是 ref,每次追加字符 Vue 都会触发重新渲染,Bubble 内部高效地更新显示内容,无需任何额外配置。
3. 头像和位置
placement 控制气泡在对话流中的对齐方向,avatar 接受 VNode 或组件来自定义头像。
vue
<template>
<div style="display: flex; flex-direction: column; gap: 16px">
<!-- 用户消息:右对齐,用户头像 -->
<tr-bubble
content="你好,帮我分析一下数据"
:avatar="userAvatar"
placement="end"
style="--tr-bubble-box-bg: var(--tr-color-primary-light)"
/>
<!-- AI 回复:左对齐,AI 头像 -->
<tr-bubble
content="好的,我来帮你分析..."
:avatar="aiAvatar"
placement="start"
/>
</div>
</template>
<script setup lang="ts">
import { TrBubble } from '@opentiny/tiny-robot'
import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
import { h } from 'vue'
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
const userAvatar = h(IconUser, { style: { fontSize: '32px' } })
</script>
placement="end" 将气泡靠右对齐(通常用于用户消息),placement="start" 靠左对齐(通常用于 AI 回复)。avatar 通过 Vue 的 h() 函数渲染 SVG 图标组件。
4. BubbleList + roleConfigs:批量消息管理
单条气泡适合演示场景,真实应用中需要用 TrBubbleList 渲染消息列表,并通过 roleConfigs 统一配置每个角色的外观。
vue
<template>
<tr-bubble-list :messages="messages" :role-configs="roles" />
</template>
<script setup lang="ts">
import { BubbleListProps, BubbleRoleConfig, TrBubbleList } from '@opentiny/tiny-robot'
import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
import { h } from 'vue'
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
const userAvatar = h(IconUser, { style: { fontSize: '32px' } })
const messages: BubbleListProps['messages'] = [
{ role: 'user', content: '用户消息 1' },
{ role: 'ai', content: 'AI 回复 1' },
{ role: 'user', content: '用户消息 2' },
{ role: 'ai', content: 'AI 回复 2' },
]
const roles: Record<string, BubbleRoleConfig> = {
ai: { placement: 'start', avatar: aiAvatar },
user: { placement: 'end', avatar: userAvatar },
}
</script>
<style scoped>
:deep([data-role='user']) {
--tr-bubble-box-bg: var(--tr-color-primary-light);
}
</style>
BubbleList 中每个消息对象包含 role 和 content 字段。roleConfigs 按 role 名称映射配置,BubbleRoleConfig 支持配置 avatar、placement、shape、hidden 等属性。
分组策略 :通过 group-strategy 属性控制消息分组方式:
'consecutive'(连续分组):连续相同角色的消息合并为一组,适用于最小化视觉干扰的对话流'divider'(分割分组,默认):以dividerRole(默认为'user')为分割线,每条分割角色消息单独成组- 自定义函数 :
(messages, dividerRole?) => BubbleMessageGroup[],完全控制分组逻辑
vue
<!-- 连续分组示例 -->
<tr-bubble-list
:messages="messages"
:role-configs="roles"
group-strategy="consecutive"
/>
5. Markdown 渲染
AI 回复通常包含丰富的格式化内容。Bubble 内置了 BubbleRenderers.Markdown 渲染器,需要安装 markdown-it 和 dompurify:
bash
pnpm add markdown-it dompurify
vue
<template>
<tr-bubble
:content="mdContent"
:avatar="aiAvatar"
:fallback-content-renderer="BubbleRenderers.Markdown"
/>
</template>
<script setup lang="ts">
import { BubbleRenderers, TrBubble } from '@opentiny/tiny-robot'
import { IconAi } from '@opentiny/tiny-robot-svgs'
import { h } from 'vue'
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
const mdContent = `# 数据分析报告
**关键发现:** 本季度营收增长 *12.5%*。
- 新用户注册量:+23%
- 活跃用户留存率:87.3%
> 数据来源:2026 Q2 内部统计
\`\`\`javascript
const growth = (current - previous) / previous * 100
console.log(\`增长率: \${growth}%\`)
\`\`\`
`
</script>
通过 fallback-content-renderer 将 Markdown 渲染器设置为降级渲染器,当内置渲染器无法匹配时使用。如果希望全局启用 Markdown 渲染,可以使用 BubbleProvider(见下文渲染器架构部分)。
6. 自定义 Content 渲染器
当内置渲染器不能满足需求时(例如自定义代码块、图表、特殊卡片),可以实现自定义 Content 渲染器。
vue
<template>
<div style="display: flex; flex-direction: column; gap: 16px">
<tr-bubble
:content="codeMessage"
:avatar="aiAvatar"
:fallback-content-renderer="CodeBlockRenderer"
/>
<tr-bubble :content="normalMessage" :avatar="aiAvatar" />
</div>
</template>
<script setup lang="ts">
import { BubbleContentRendererProps, TrBubble, useMessageContent } from '@opentiny/tiny-robot'
import { IconAi } from '@opentiny/tiny-robot-svgs'
import { defineComponent, h } from 'vue'
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
// 自定义消息类型
interface CodeMessage {
type: 'code'
language: string
code: string
}
const codeMessage: CodeMessage[] = [
{
type: 'code',
language: 'typescript',
code: `interface User {
name: string
age: number
}
const fetchUser = async (id: string): Promise<User> => {
const res = await fetch(\`/api/users/\${id}\`)
return res.json()
}`,
},
]
const normalMessage = '这是一条普通文本消息'
// 自定义代码块渲染器
const CodeBlockRenderer = defineComponent({
props: {
message: { type: Object, required: true },
contentIndex: Number,
},
setup(props: BubbleContentRendererProps) {
// 使用 useMessageContent 正确处理数组内容和 contentIndex
const { content: contentItem } = useMessageContent(props)
return () => {
const content = contentItem.value as unknown as CodeMessage
if (!content || content.type !== 'code') {
return h('div', '无效的代码内容')
}
return h('div', { class: 'code-block-wrapper' }, [
h('div', { class: 'code-block-header' }, content.language || 'code'),
h('pre', { class: 'code-block-content' }, h('code', {}, content.code)),
])
}
},
})
</script>
<style scoped>
.code-block-wrapper {
width: 100%;
max-width: 100%;
}
.code-block-header {
padding: 8px 12px;
background: #2d2d2d;
color: #fff;
font-size: 12px;
border-radius: 6px 6px 0 0;
}
.code-block-content {
margin: 0;
padding: 12px;
background: #1e1e1e;
color: #d4d4d4;
font-size: 14px;
font-family: monospace;
border-radius: 0 0 6px 6px;
overflow: auto;
}
</style>
自定义 Content 渲染器接收 BubbleContentRendererProps(包含 message 和 contentIndex)。推荐使用 useMessageContent(props) 辅助函数来获取当前内容项------它会根据 contentIndex 自动从数组中提取正确的元素。
7. 状态管理与交互
Bubble 的 state 属性用于存储 UI 相关数据(不影响消息内容),通过 state-change 事件更新状态。这非常适合实现展开/收起、点赞、复制等交互功能。
vue
<template>
<div style="display: flex; flex-direction: column; gap: 16px">
<div>
<label>
<input type="checkbox" v-model="messageState.expanded" />
展开详情
</label>
</div>
<tr-bubble
content="这是一条可以交互的消息,支持展开查看详情和点赞操作。"
:avatar="aiAvatar"
:state="messageState"
@state-change="handleStateChange"
>
<template #content-footer>
<div
v-if="messageState.expanded"
style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #eee"
>
<p style="margin: 0 0 8px; font-size: 14px; color: #666">
详细信息:该消息包含扩展内容,可用于展示补充说明。
</p>
<button @click="toggleLike" style="padding: 4px 12px; font-size: 12px; cursor: pointer">
{{ messageState.liked ? '❤️ 已点赞' : '🤍 点赞' }}
</button>
</div>
</template>
</tr-bubble>
</div>
</template>
<script setup lang="ts">
import { TrBubble } from '@opentiny/tiny-robot'
import { IconAi } from '@opentiny/tiny-robot-svgs'
import { h, ref } from 'vue'
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
const messageState = ref<Record<string, unknown>>({
expanded: false,
liked: false,
})
const handleStateChange = (payload: { key: string; value: unknown }) => {
messageState.value[payload.key] = payload.value
}
const toggleLike = () => {
messageState.value.liked = !messageState.value.liked
handleStateChange({ key: 'liked', value: messageState.value.liked })
}
</script>
state-change 事件的参数结构为 { key, value, messageIndex, contentIndex },其中 messageIndex 和 contentIndex 在 BubbleList 中用于定位具体消息。
渲染器架构深度解析
TinyRobot Bubble 的核心设计是渲染器架构------将"容器样式"和"内容渲染"解耦为两种独立的渲染器。
Box 渲染器 vs Content 渲染器
| 类型 | 职责 | Props | 典型场景 |
|---|---|---|---|
| Box 渲染器 | 渲染气泡外层容器,控制样式和布局 | placement, shape |
自定义气泡形状、背景、阴影 |
| Content 渲染器 | 渲染消息具体内容 | message, contentIndex |
文本、图片、Markdown、代码等 |
每种渲染器都通过匹配规则 (Match)来决定何时激活。匹配规则包含 find 函数和 priority 优先级。
渲染器匹配机制
匹配过程遵循以下步骤:
- 按
priority从小到大排序所有规则(值越小优先级越高) - 依次执行每个规则的
find函数,找到第一个返回true的规则 - 使用该规则对应的渲染器
- 如果没有匹配到任何规则,使用 fallback 渲染器
优先级常量 (通过 BubbleRendererMatchPriority 访问):
| 常量 | 数值 | 触发条件示例 |
|---|---|---|
LOADING |
-1 | message.loading === true |
NORMAL |
0 | 默认优先级,常规规则 |
CONTENT |
10 | content.type === 'image_url' |
ROLE |
20 | message.role === 'tool' |
配置层级
渲染器可以在三个层级配置,优先级从高到低:
- Prop 级别 :直接在
TrBubble上设置fallback-box-renderer/fallback-content-renderer,仅对当前组件生效 - Provider 级别 :通过
BubbleProvider的box-renderer-matches/content-renderer-matches配置,在整个组件树中生效 - Default 级别:内置的默认渲染器
通过 BubbleProvider 全局配置渲染器
vue
<template>
<tr-bubble-provider
:box-renderer-matches="boxRendererMatches"
:content-renderer-matches="contentRendererMatches"
:fallback-content-renderer="BubbleRenderers.Markdown"
>
<!-- 子组件中的所有 Bubble 自动获得这些渲染器配置 -->
<tr-bubble-list :messages="messages" :role-configs="roles" />
</tr-bubble-provider>
</template>
<script setup lang="ts">
import {
BubbleBoxRendererMatch,
BubbleContentRendererMatch,
BubbleRendererMatchPriority,
BubbleRenderers,
TrBubbleProvider,
} from '@opentiny/tiny-robot'
import { markRaw } from 'vue'
const boxRendererMatches: BubbleBoxRendererMatch[] = [
{
// find 签名:(messages, content, contentIndex) => boolean
find: (messages, _content, _contentIndex) =>
messages.some((m) => typeof m.content === 'string' && m.content.includes('VIP')),
renderer: markRaw(CustomBoxRenderer),
priority: BubbleRendererMatchPriority.NORMAL,
},
]
const contentRendererMatches: BubbleContentRendererMatch[] = [
{
// find 签名:(message, content, contentIndex) => boolean
find: (message, content) => content.type === 'code',
renderer: markRaw(CodeBlockRenderer),
priority: BubbleRendererMatchPriority.CONTENT,
},
]
</script>
内置渲染器一览
通过 BubbleRenderers 访问:
| 渲染器 | 类型 | 说明 |
|---|---|---|
BubbleRenderers.Box |
Box | 默认气泡容器 |
BubbleRenderers.Text |
Content | 文本内容(默认) |
BubbleRenderers.Image |
Content | 图片(type: 'image_url') |
BubbleRenderers.Markdown |
Content | Markdown 渲染 |
BubbleRenderers.Loading |
Content | 加载动画 |
BubbleRenderers.Reasoning |
Content | 推理过程展示 |
BubbleRenderers.Tool |
Content | 单个工具调用 |
BubbleRenderers.Tools |
Content | 工具调用列表 |
BubbleRenderers.ToolRole |
Content | 工具角色消息 |
推理内容 (Reasoning)用于展示 AI 的思考过程,通过 reasoning_content 属性传入:
vue
<template>
<tr-bubble
:content="reply"
:reasoning_content="thinking"
:state="{ open: true }"
:avatar="aiAvatar"
/>
</template>
<script setup lang="ts">
import { TrBubble } from '@opentiny/tiny-robot'
const thinking = `分析用户问题:二进制加法规则...
1+1 在二进制中产生进位,结果为 10。`
const reply = `二进制中 1+1 的结果是 10。`
</script>
CSS 变量定制
TinyRobot Bubble 提供了 50+ 个 CSS 变量,覆盖从根容器到各子模块的样式。以下是核心变量的分类概览:
Bubble 根元素
| 变量 | 说明 | 默认值 |
|---|---|---|
--tr-bubble-gap |
头像与内容间距 | 8px |
--tr-bubble-max-width |
气泡最大宽度 | 80% |
--tr-bubble-min-width |
气泡最小宽度 | - |
Box 容器
| 变量 | 说明 |
|---|---|
--tr-bubble-box-bg |
背景色 |
--tr-bubble-box-padding |
内边距 |
--tr-bubble-box-border-radius |
圆角 |
--tr-bubble-box-shadow |
阴影 |
--tr-bubble-box-border |
边框 |
--tr-bubble-box-shape-rounded-radius |
rounded 形状圆角 |
--tr-bubble-box-shape-corner-radius |
corner 形状的特定角圆角 |
文本
| 变量 | 说明 |
|---|---|
--tr-bubble-text-color |
文字颜色 |
--tr-bubble-text-font-size |
字号 |
--tr-bubble-text-line-height |
行高 |
加载状态
| 变量 | 说明 |
|---|---|
--tr-bubble-loading-color |
加载图标颜色 |
--tr-bubble-loading-size |
加载图标尺寸 |
图片
| 变量 | 说明 |
|---|---|
--tr-bubble-image-max-width |
图片最大宽度 |
--tr-bubble-image-max-height |
图片最大高度 |
--tr-bubble-image-border-radius |
图片圆角 |
推理内容
| 变量 | 说明 |
|---|---|
--tr-bubble-reasoning-max-height |
推理区域最大高度 |
--tr-bubble-reasoning-side-border-width |
左侧边线宽度 |
--tr-bubble-reasoning-side-border-color |
左侧边线颜色 |
BubbleList 容器
| 变量 | 说明 |
|---|---|
--tr-bubble-list-gap |
气泡间距 |
--tr-bubble-list-padding |
容器内边距 |
所有变量均可通过 style 属性或 CSS 类覆盖:
css
:deep([data-role='user']) {
--tr-bubble-box-bg: #e3f2fd;
--tr-bubble-text-font-size: 15px;
--tr-bubble-box-shape-rounded-radius: 16px;
}
:deep([data-role='ai']) {
--tr-bubble-box-bg: #ffffff;
--tr-bubble-box-border: 1px solid #e8e8e8;
}
总结
TinyRobot Bubble 组件通过渲染器架构 、消息分组 、流式支持 和灵活的 CSS 变量,为 AI 对话界面提供了完整的解决方案。它的设计哲学是"约定优于配置"------默认行为足够好用,但每个环节都开放了扩展点:
- 需要自定义渲染逻辑?实现 Content 渲染器
- 需要改气泡形态?实现 Box 渲染器
- 需要简单的样式调整?覆盖 CSS 变量
- 需要在全局统一渲染策略?使用 BubbleProvider
从简单的文本气泡到复杂的工具调用展示,从单条消息到分组列表,Bubble 组件能够在各种复杂度下保持一致的使用体验。
OpenTiny NEXT 是一套企业智能前端开发解决方案,以生成式 UI 和 WebMCP 两大核心技术为基础,对现有传统的 TinyVue 组件库、TinyEngine 低代码引擎等产品进行智能化升级,构建出面向 Agent 应用的前端 NEXT-SDKs、AI Extension、TinyRobot智能助手、GenUI等新产品,加速企业应用的智能化改造,实现AI理解用户意图自主完成任务。
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design/ TinyRobot 代码仓库:github.com/opentiny/ti... (欢迎star ⭐) TinyRobot skill源码:github.com/opentiny/ag... (欢迎 Star ⭐)