用TinyRobot Bubble组件打造灵活强大的AI对话气泡

用 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>

关键点:contentref,每次追加字符 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 中每个消息对象包含 rolecontent 字段。roleConfigs 按 role 名称映射配置,BubbleRoleConfig 支持配置 avatarplacementshapehidden 等属性。

分组策略 :通过 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-itdompurify

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(包含 messagecontentIndex)。推荐使用 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 },其中 messageIndexcontentIndex 在 BubbleList 中用于定位具体消息。

渲染器架构深度解析

TinyRobot Bubble 的核心设计是渲染器架构------将"容器样式"和"内容渲染"解耦为两种独立的渲染器。

Box 渲染器 vs Content 渲染器

类型 职责 Props 典型场景
Box 渲染器 渲染气泡外层容器,控制样式和布局 placement, shape 自定义气泡形状、背景、阴影
Content 渲染器 渲染消息具体内容 message, contentIndex 文本、图片、Markdown、代码等

每种渲染器都通过匹配规则 (Match)来决定何时激活。匹配规则包含 find 函数和 priority 优先级。

渲染器匹配机制

匹配过程遵循以下步骤:

  1. priority 从小到大排序所有规则(值越小优先级越高)
  2. 依次执行每个规则的 find 函数,找到第一个返回 true 的规则
  3. 使用该规则对应的渲染器
  4. 如果没有匹配到任何规则,使用 fallback 渲染器

优先级常量 (通过 BubbleRendererMatchPriority 访问):

常量 数值 触发条件示例
LOADING -1 message.loading === true
NORMAL 0 默认优先级,常规规则
CONTENT 10 content.type === 'image_url'
ROLE 20 message.role === 'tool'

配置层级

渲染器可以在三个层级配置,优先级从高到低:

  1. Prop 级别 :直接在 TrBubble 上设置 fallback-box-renderer / fallback-content-renderer,仅对当前组件生效
  2. Provider 级别 :通过 BubbleProviderbox-renderer-matches / content-renderer-matches 配置,在整个组件树中生效
  3. 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 ⭐)

相关推荐
橘子星1 小时前
打破串行枷锁:深入理解 JS 同步、异步与 Promise 实战
前端·javascript
用户059540174461 小时前
LangChain 记忆模块踩坑实录:靠自动化测试,我把上下文丢失率从 30% 降到 0
前端·css
kismet7871 小时前
fetch 正常,页面却 404?Nuxt 3 + CDN 跨域下的 preload CORS 陷阱
前端·产品
如果超人不会飞1 小时前
新手避坑:使用 TinyRobot 入门阶段常见误区总结
前端·vue.js
嘟嘟07171 小时前
二叉树从入门到实战:四大遍历 + 递归思想详解
前端
渣波1 小时前
全栈开发的“影分身”之术(mock):别再手动造数据了,你的 CRUD 不配让我等!
前端·javascript
亿元程序员1 小时前
小伙伴说这个撕胶带游戏很火很解压,于是我连夜做了一个Cocos教程...
前端
如果超人不会飞1 小时前
一文读懂 TinyRobot:前端 AI 组件库定位、价值与适用场景
前端·vue.js
如果超人不会飞1 小时前
用TinyRobot Welcome组件打造贴心的AI助手欢迎页
前端·vue.js