组件联动进阶:玩转 TinyRobot 组合开发,提升项目灵活性
AI 聊天应用不是单个组件的孤岛,而是多个组件协同工作的生态系统。用户输入消息(Sender)、查看消息列表(BubbleList)、选择提示(Prompts)、管理会话(useConversation)------这些环节需要无缝衔接,数据需要实时同步,状态需要一致联动。
TinyRobot 的组件设计遵循组合优于配置的原则,每个组件都是独立可用的,但通过事件、方法、组合式函数的衔接,可以构建出完整的聊天应用。本文将从 7 个联动场景出发,逐步构建一个完整的 AI 聊天应用。
Sender + BubbleList:输入与展示联动
这是最基础的联动模式:用户通过 Sender 输入消息,BubbleList 展示消息列表。
sendMessage 流程
核心流程:Sender submit → 创建 user 消息 → 创建 assistant 消息(loading)→ 请求 AI 响应 → 更新 assistant 消息内容。
vue
<template>
<tr-bubble-list
:messages="messages"
:role-configs="roleConfigs"
:auto-scroll="true"
/>
<tr-sender
:loading="isLoading"
@submit="handleSubmit"
@cancel="handleCancel"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const messages = ref([])
const isLoading = ref(false)
const roleConfigs = {
user: { placement: 'end', shape: 'corner' },
assistant: { placement: 'start', shape: 'corner' }
}
const handleSubmit = async (text: string) => {
// 1. 添加用户消息
messages.value.push({
role: 'user',
content: text,
id: `user-${Date.now()}`
})
// 2. 添加助手消息(loading 状态)
const assistantId = `assistant-${Date.now()}`
messages.value.push({
role: 'assistant',
content: '',
loading: true,
id: assistantId
})
isLoading.value = true
// 3. 请求 AI 响应
try {
const response = await fetchAIResponse(text)
// 4. 更新助手消息内容
const msg = messages.value.find(m => m.id === assistantId)
if (msg) {
msg.content = response
msg.loading = false
}
} catch (error) {
const msg = messages.value.find(m => m.id === assistantId)
if (msg) {
msg.content = '请求失败,请重试'
msg.loading = false
}
} finally {
isLoading.value = false
}
}
const handleCancel = () => {
isLoading.value = false
// 将最后一条 loading 的助手消息标记为已终止
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg?.loading) {
lastMsg.loading = false
}
}
</script>
loading 状态同步
Sender 的 loading 属性与 BubbleList 的消息 loading 状态需要同步:
- Sender
loading = true→ 提交按钮变为停止按钮 - BubbleList 中助手消息
loading = true→ 显示加载动画
cancel 取消操作
点击 Sender 的停止按钮触发 cancel 事件,需要同时:
- 中止 AI 响应请求(如
abortRequest()) - 将 BubbleList 中 loading 状态的助手消息终止
Sender + Prompts:提示集与输入联动
Prompts 组件展示预设的提示列表,用户点击提示项后,文本填入 Sender。
item-click 事件与 setContent 方法
vue
<template>
<tr-prompts
:items="promptItems"
@item-click="handlePromptClick"
/>
<tr-sender ref="senderRef" @submit="handleSubmit" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const senderRef = ref()
const promptItems = [
{ label: '帮我写一篇周报', description: '根据本周工作内容生成周报' },
{ label: '翻译以下内容', description: '将文本翻译为指定语言' },
{ label: '代码解释', description: '解释一段代码的逻辑和功能' },
{ label: '数据分析', description: '对给定数据进行分析和可视化建议' }
]
const handlePromptClick = (ev: MouseEvent, item: PromptProps) => {
// 将提示文本填入 Sender
senderRef.value?.setContent(item.label)
senderRef.value?.focus()
}
const handleSubmit = (text: string) => {
console.log('提交:', text)
}
</script>
关键衔接点:
- Prompts 的
item-click事件提供(ev, item)参数 - Sender 的
setContent()方法设置编辑器内容(v0.4 新增) - Sender 的
focus()方法让编辑器获取焦点
这种"提示点击 → 内容填入 → 聚焦编辑器"的流程是 AI 聊天应用中最常见的交互模式。
Sender + Container:容器内嵌布局
Container 组件提供聊天界面的容器框架,Sender 通常放置在 Container 的 footer 插槽中。
全屏模式下的布局适配
vue
<template>
<tr-container
v-model:show="showContainer"
v-model:fullscreen="isFullscreen"
title="AI 助手"
>
<tr-bubble-list :messages="messages" :auto-scroll="true" />
<template #footer>
<tr-sender
v-model="inputText"
:loading="isLoading"
@submit="handleSubmit"
@cancel="handleCancel"
/>
</template>
</tr-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const showContainer = ref(true)
const isFullscreen = ref(false)
const inputText = ref('')
const isLoading = ref(false)
const messages = ref([])
const handleSubmit = async (text: string) => {
// 发送消息逻辑
}
const handleCancel = () => {
isLoading.value = false
}
</script>
全屏模式下,Container 会加上 fullscreen 类名,此时可以使用 .fullscreen 选择器自定义 footer 插槽中 Sender 的样式:
css
/* 全屏模式下 Sender 的样式调整 */
.fullscreen .tr-sender {
--tr-sender-bg-color: #fff;
--tr-sender-border-radius: 8px;
}
Container 的 CSS 变量也提供了全屏模式专属变量:
css
:root {
--tr-container-header-padding-fullscreen: 0 200px 24px;
--tr-container-title-font-size-fullscreen: 20px;
}
Sender + Feedback:反馈与气泡联动
Feedback 组件提供对气泡消息的操作反馈(复制、点赞、刷新等),通常放置在 Bubble 的 content-footer 插槽中。
vue
<template>
<tr-bubble-list :messages="messages" :role-configs="roleConfigs">
<template #content-footer="{ messages, role }">
<!-- 仅对助手消息显示反馈 -->
<tr-feedback
v-if="role === 'assistant'"
:actions="feedbackActions"
@action="handleAction"
/>
</template>
</tr-bubble-list>
</template>
<script setup lang="ts">
const feedbackActions = [
{ name: 'copy', label: '复制' },
{ name: 'like', label: '点赞' },
{ name: 'dislike', label: '踩' },
{ name: 'refresh', label: '重新生成' }
]
const handleAction = (name: string) => {
if (name === 'refresh') {
// 重新触发 Sender 提交
senderRef.value?.submit()
}
if (name === 'copy') {
// 复制消息内容
navigator.clipboard.writeText(currentMessage.content)
}
}
</script>
Sender + useMessage:数据管理与输入联动
useMessage 是 TinyRobot 提供的消息数据管理组合式函数,它封装了发送消息、处理 AI 响应、管理请求状态等逻辑。与 Sender 联动后,开发者无需手动管理消息数组。
sendMessage 与 Sender.submit 对接
vue
<template>
<tr-bubble-list
:messages="messageEngine.messages.value"
:auto-scroll="true"
/>
<tr-sender
:loading="messageEngine.isProcessing.value"
@submit="handleSenderSubmit"
@cancel="handleSenderCancel"
/>
</template>
<script setup lang="ts">
import { useMessage } from '@opentiny/tiny-robot'
// 使用 useMessage 管理消息数据
const messageEngine = useMessage({
responseProvider: async (requestBody, abortSignal) => {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
signal: abortSignal
})
// 处理 SSE 流式响应
return sseStreamToGenerator(response)
}
})
// Sender submit → useMessage.sendMessage
const handleSenderSubmit = (text: string) => {
messageEngine.sendMessage(text)
}
// Sender cancel → useMessage.abortRequest
const handleSenderCancel = () => {
messageEngine.abortRequest()
}
</script>
requestState 驱动 Sender 的 loading/disabled
useMessage 的 requestState 提供了更精细的状态控制:
typescript
type RequestState = 'idle' | 'processing' | 'completed' | 'aborted' | 'error'
vue
<template>
<tr-sender
:loading="messageEngine.isProcessing.value"
:disabled="messageEngine.requestState.value === 'error'"
@submit="handleSenderSubmit"
@cancel="handleSenderCancel"
/>
</template>
isProcessing→ Sender 的loading(显示停止按钮)requestState === 'error'→ Sender 的disabled(出错时禁用输入)
这种联动让 Sender 的状态完全由 useMessage 驱动,开发者无需手动管理 isLoading 状态。
Sender + useConversation:多会话联动
useConversation 在 useMessage 基础上增加了多会话管理能力。每个会话有独立的 useMessage 引擎,切换会话时需要同步更新 Sender 的状态。
activeConversation 与 Sender 联动
vue
<template>
<!-- 会话列表 -->
<div class="conversation-list">
<div
v-for="conv in conversationEngine.conversations.value"
:key="conv.id"
:class="{ active: conv.id === conversationEngine.activeConversationId.value }"
@click="switchConversation(conv.id)"
>
{{ conv.title || '新对话' }}
</div>
<button @click="createNewConversation">新建对话</button>
</div>
<!-- 消息展示 -->
<tr-bubble-list
:messages="currentMessages"
:auto-scroll="true"
/>
<!-- 输入框 -->
<tr-sender
:loading="currentEngine?.isProcessing.value"
@submit="handleSenderSubmit"
@cancel="handleSenderCancel"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useConversation } from '@opentiny/tiny-robot'
const conversationEngine = useConversation({
useMessageOptions: {
responseProvider: async (requestBody, abortSignal) => {
// ...请求逻辑
}
}
})
// 当前会话的消息引擎
const currentEngine = computed(() =>
conversationEngine.activeConversation.value?.engine
)
// 当前会话的消息列表
const currentMessages = computed(() =>
currentEngine?.messages.value || []
)
// 切换会话
const switchConversation = (id: string) => {
conversationEngine.switchConversation(id)
}
// 创建新会话
const createNewConversation = () => {
conversationEngine.createConversation({ title: '新对话' })
}
// Sender submit → 当前会话的 sendMessage
const handleSenderSubmit = (text: string) => {
conversationEngine.sendMessage(text)
}
// Sender cancel → 当前会话的 abortRequest
const handleSenderCancel = () => {
conversationEngine.abortActiveRequest()
}
</script>
关键联动点:
- 切换会话 :
switchConversation()→ 更新activeConversation→ 更新currentMessages→ BubbleList 自动渲染新会话的消息 - 发送消息 :
sendMessage()→ 当前会话的 useMessage 引擎处理 - 取消请求 :
abortActiveRequest()→ 当前会话的请求被中止 - 并行处理:切换会话后,旧会话的请求可以在后台继续执行
Sender + ThemeProvider:主题联动
ThemeProvider 的主题切换会影响所有子组件的样式,包括 Sender。
vue
<template>
<theme-provider
v-model:theme="currentTheme"
v-model:color-mode="colorMode"
>
<tr-sender v-model="text" @submit="handleSubmit" />
</theme-provider>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ThemeProvider } from '@opentiny/tiny-robot'
const currentTheme = ref('')
const colorMode = ref('auto')
const text = ref('')
const handleSubmit = (content: string) => {
console.log('提交:', content)
}
</script>
通过 CSS 属性选择器定制 Sender 在不同主题下的样式:
css
/* 亮色模式 */
[data-tr-color-mode='light'] {
--tr-sender-bg-color: #ffffff;
--tr-sender-text-color: #333333;
--tr-sender-placeholder-color: #999999;
}
/* 暗色模式 */
[data-tr-color-mode='dark'] {
--tr-sender-bg-color: #1a1a2e;
--tr-sender-text-color: #e0e0e0;
--tr-sender-placeholder-color: #666666;
}
/* 自定义品牌主题 */
[data-tr-theme='brand'] {
--tr-sender-bg-color: #f0f7ff;
--tr-sender-border-radius: 16px;
--tr-sender-button-size: 36px;
}
实战组合案例:完整 AI 聊天应用
将上述所有联动场景组合,构建一个完整的 AI 聊天应用:
vue
<template>
<theme-provider
v-model:theme="theme"
v-model:color-mode="colorMode"
:storage="localStorage"
storage-key="chat-app-theme"
>
<tr-container
v-model:show="showChat"
v-model:fullscreen="isFullscreen"
title="AI 智能助手"
>
<!-- 会话切换 -->
<div class="sidebar">
<div
v-for="conv in conversation.conversations.value"
:key="conv.id"
:class="{ active: conv.id === conversation.activeConversationId.value }"
@click="conversation.switchConversation(conv.id)"
>
{{ conv.title || '新对话' }}
</div>
<button @click="createConversation">+ 新对话</button>
</div>
<!-- 消息展示区 -->
<div class="chat-main">
<!-- 提示集(无消息时显示) -->
<tr-prompts
v-if="currentMessages.length === 0"
:items="promptItems"
@item-click="handlePromptClick"
/>
<!-- 消息列表 -->
<tr-bubble-list
v-else
:messages="currentMessages"
:role-configs="roleConfigs"
:auto-scroll="true"
>
<template #content-footer="{ role }">
<tr-feedback
v-if="role === 'assistant' && !messageLoading"
:actions="feedbackActions"
@action="handleFeedbackAction"
/>
</template>
</tr-bubble-list>
</div>
<!-- 输入框 -->
<template #footer>
<tr-sender
ref="senderRef"
v-model="inputText"
:loading="isProcessing"
:default-actions="{
submit: { tooltip: '发送(Enter)' },
clear: { tooltip: '清空' }
}"
@submit="handleSubmit"
@cancel="handleCancel"
>
<template #footer="{ disabled, loading, insert }">
<tr-voice-button
:disabled="disabled || loading"
:speech-config="{ lang: 'zh-CN' }"
/>
<tr-upload-button
:disabled="disabled || loading"
accept="image/*,.pdf"
:max-size="20"
@select="handleFileSelect"
/>
</template>
</tr-sender>
</template>
</tr-container>
</theme-provider>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
ThemeProvider,
useConversation,
useTheme
} from '@opentiny/tiny-robot'
// 主题配置
const theme = ref('')
const colorMode = ref('auto')
const { toggleColorMode, setTheme } = useTheme()
// 会话管理
const conversation = useConversation({
useMessageOptions: {
responseProvider: async (requestBody, abortSignal) => {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
signal: abortSignal
})
return sseStreamToGenerator(response)
}
}
})
// 当前会话状态
const currentEngine = computed(() =>
conversation.activeConversation.value?.engine
)
const currentMessages = computed(() => currentEngine?.messages.value || [])
const isProcessing = computed(() => currentEngine?.isProcessing.value || false)
// 组件引用
const senderRef = ref()
const inputText = ref('')
const showChat = ref(true)
const isFullscreen = ref(false)
// 提示项
const promptItems = [
{ label: '帮我写一篇周报', description: '根据本周工作内容生成周报' },
{ label: '翻译以下内容', description: '将文本翻译为指定语言' },
{ label: '代码解释', description: '解释一段代码的逻辑和功能' }
]
// 角色配置
const roleConfigs = {
user: { placement: 'end', shape: 'corner' },
assistant: { placement: 'start', shape: 'corner' }
}
// 反馈操作
const feedbackActions = [
{ name: 'copy', label: '复制' },
{ name: 'like', label: '点赞' },
{ name: 'refresh', label: '重新生成' }
]
// 事件处理
const handleSubmit = (text: string) => {
conversation.sendMessage(text)
}
const handleCancel = () => {
conversation.abortActiveRequest()
}
const handlePromptClick = (ev, item) => {
senderRef.value?.setContent(item.label)
senderRef.value?.focus()
}
const handleFileSelect = (files: File[]) => {
console.log('文件:', files)
}
const handleFeedbackAction = (name: string) => {
if (name === 'refresh') senderRef.value?.submit()
}
const createConversation = () => {
conversation.createConversation({ title: '新对话' })
}
</script>
组合开发模式总结
TinyRobot 的组合开发遵循以下模式:
| 联动场景 | 衔接机制 | 核心方法/事件 |
|---|---|---|
| Sender + BubbleList | 事件 + 数据 | submit → push message, cancel → abort |
| Sender + Prompts | 事件 + 方法 | item-click → setContent + focus |
| Sender + Container | 插槽 | footer 插槽放置 Sender |
| Sender + Feedback | 插槽 + 事件 | content-footer 插槽, action → submit |
| Sender + useMessage | 组合式函数 | sendMessage, isProcessing, abortRequest |
| Sender + useConversation | 组合式函数 | sendMessage, abortActiveRequest, switchConversation |
| Sender + ThemeProvider | CSS 变量 | data-tr-theme / data-tr-color-mode 属性选择器 |
核心原则:组件之间通过事件和方法衔接,而不是通过共享状态或直接引用。这种松耦合设计让每个组件都可以独立使用、独立测试,组合时只需要"接线"即可。
🔗 TinyRobot 官网 :tiny-robot.opentiny.design
🔗 GitHub 仓库 :github.com/opentiny/ti...