diff --git a/docs/demos/bubble/list-array-content.vue b/docs/demos/bubble/list-array-content.vue
index 429905201..50e295d86 100644
--- a/docs/demos/bubble/list-array-content.vue
+++ b/docs/demos/bubble/list-array-content.vue
@@ -1,9 +1,10 @@
- 当 message.role === 'user' 且 content 为数组时,该消息会被单独分组(密封),后续消息不会合并到该组。
+ 满足「contentRenderMode 为 split 且组内只有 1 条消息」时,数组 content 的每一项会单独渲染为一个 box; 否则在同一
+ box 内渲染。下例中第一个气泡满足该条件(单条消息 + 数组 content + split),故出现多个 box。
-
+
@@ -15,24 +16,24 @@ import { h } from 'vue'
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
const userAvatar = h(IconUser, { style: { fontSize: '32px' } })
+// 第一个气泡:单条消息 + content 为数组,且 contentRenderMode="split" → 每项单独一个 box
+// 第二、三个气泡:单条消息 + content 为字符串 → 各一个 box
const messages: BubbleListProps['messages'] = [
{
role: 'user',
- // role 为 user 且 content 为数组时,会被单独分组(密封)
content: [
- { type: 'text', text: '第一部分' },
- { type: 'text', text: '第二部分' },
+ { type: 'text', text: '数组第一项' },
+ { type: 'text', text: '数组第二项' },
+ { type: 'text', text: '数组第三项' },
],
},
{
role: 'ai',
- // 上一条为 user+数组(密封),所以这条单独成组
- content: '第二条消息(单独成组)',
+ content: '单条消息,字符串 content,一个 box',
},
{
- role: 'ai',
- // 与上一条角色相同且上一条非密封,合并到同一组
- content: '第三条消息(与第二条合并)',
+ role: 'user',
+ content: '单条消息,字符串 content,一个 box',
},
]
diff --git a/docs/demos/bubble/list-consecutive.vue b/docs/demos/bubble/list-consecutive.vue
index efab1c65d..3d001de19 100644
--- a/docs/demos/bubble/list-consecutive.vue
+++ b/docs/demos/bubble/list-consecutive.vue
@@ -9,7 +9,7 @@
divider 分组策略(对比)
- 按分割角色分组,连续的分割角色在一组,其他消息在另一组
+ 按分割角色分组(每条分割角色消息单独成组,其他消息在两个分割角色之间合并为一组)
@@ -42,6 +42,7 @@ const systemAvatar = h(
'S',
)
+// consecutive:连续相同角色合并为一组;divider:每条分割角色单独成组,其他消息在两分割角色之间合并为一组
const messages: BubbleListProps['messages'] = [
{
role: 'user',
@@ -49,7 +50,7 @@ const messages: BubbleListProps['messages'] = [
},
{
role: 'user',
- content: '第二条用户消息(连续,会被合并)',
+ content: '第二条用户消息',
},
{
role: 'ai',
@@ -57,7 +58,7 @@ const messages: BubbleListProps['messages'] = [
},
{
role: 'ai',
- content: 'AI 回复第二条(连续,会被合并)',
+ content: 'AI 回复第二条',
},
{
role: 'system',
@@ -65,7 +66,7 @@ const messages: BubbleListProps['messages'] = [
},
{
role: 'system',
- content: '系统通知:这是另一条系统消息(连续,会被合并)',
+ content: '系统通知:另一条系统消息',
},
{
role: 'user',
diff --git a/docs/demos/bubble/list-custom-group.vue b/docs/demos/bubble/list-custom-group.vue
index 830aadb3d..af8c21f9b 100644
--- a/docs/demos/bubble/list-custom-group.vue
+++ b/docs/demos/bubble/list-custom-group.vue
@@ -1,8 +1,32 @@
- 自定义分组函数:按时间间隔分组,时间间隔超过 5 秒的消息分为不同组。
+ 通过自定义分组函数控制 BubbleList 的展示逻辑:
+
+ - 「按时间间隔分组」:时间间隔超过 5 秒则开启新分组
+
+ - 「按对话轮次分组」:每一轮 user 提问及其后续 ai/system 回复视为一组
+
+
+
+
+
+
@@ -16,18 +40,22 @@ import {
TrBubbleList,
} from '@opentiny/tiny-robot'
import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
-import { h } from 'vue'
+import { h, ref } from 'vue'
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
const userAvatar = h(IconUser, { style: { fontSize: '32px' } })
-// 示例消息,包含时间戳
-const messages: (BubbleListProps['messages'][0] & { timestamp?: number })[] = [
- { role: 'user', content: '第一条消息', timestamp: 1000 },
- { role: 'user', content: '第二条消息(1秒后,同一组)', timestamp: 2000 },
- { role: 'ai', content: 'AI 回复', timestamp: 3000 },
- { role: 'user', content: '第三条消息(10秒后,新组)', timestamp: 14000 },
- { role: 'user', content: '第四条消息(1秒后,同一组)', timestamp: 15000 },
+// 示例消息,包含时间戳,方便进行时间分组演示
+type MessageWithTimestamp = BubbleListProps['messages'][0] & { timestamp?: number }
+
+const messages: MessageWithTimestamp[] = [
+ { role: 'user', content: '用户:第一次提问(t=0s)', timestamp: 0 },
+ { role: 'ai', content: 'AI:第一次回答(t=1s,同一轮对话)', timestamp: 1000 },
+ { role: 'system', content: 'System:提示信息(t=2s,同一轮对话)', timestamp: 2000 },
+ { role: 'user', content: '用户:第二次提问(t=10s,新一轮对话)', timestamp: 10000 },
+ { role: 'ai', content: 'AI:第二次回答(t=11s,同一轮对话)', timestamp: 11000 },
+ { role: 'user', content: '用户:第三次提问(t=25s,新一轮对话)', timestamp: 25000 },
+ { role: 'ai', content: 'AI:第三次回答(t=35s,时间间隔较大)', timestamp: 35000 },
]
const roles: Record = {
@@ -39,25 +67,30 @@ const roles: Record = {
placement: 'end',
avatar: userAvatar,
},
+ system: {
+ placement: 'start',
+ },
}
-// 自定义分组函数:按时间间隔分组(超过 5 秒分为不同组)
-const customGroupStrategy = (msgs: BubbleMessage[], _dividerRole?: string): BubbleMessageGroup[] => {
+// 当前分组模式:'time' | 'turn'
+const activeMode = ref<'time' | 'turn'>('time')
+
+// 按时间间隔分组:相邻消息时间差超过 5 秒则开启新分组
+const groupByTime = (msgs: BubbleMessage[]): BubbleMessageGroup[] => {
const groups: BubbleMessageGroup[] = []
const TIME_THRESHOLD = 5000
for (const [index, message] of msgs.entries()) {
- const msgWithTimestamp = message as (typeof messages)[0]
+ const msgWithTimestamp = message as MessageWithTimestamp
const lastGroup = groups[groups.length - 1]
if (
!lastGroup ||
- (msgWithTimestamp.timestamp &&
- lastGroup.messages.length > 0 &&
- (lastGroup.messages[lastGroup.messages.length - 1] as (typeof messages)[0]).timestamp &&
- msgWithTimestamp.timestamp -
- ((lastGroup.messages[lastGroup.messages.length - 1] as (typeof messages)[0]).timestamp || 0) >
- TIME_THRESHOLD)
+ !msgWithTimestamp.timestamp ||
+ !(lastGroup.messages[lastGroup.messages.length - 1] as MessageWithTimestamp).timestamp ||
+ msgWithTimestamp.timestamp -
+ ((lastGroup.messages[lastGroup.messages.length - 1] as MessageWithTimestamp).timestamp || 0) >
+ TIME_THRESHOLD
) {
groups.push({
role: message.role || 'assistant',
@@ -73,6 +106,67 @@ const customGroupStrategy = (msgs: BubbleMessage[], _dividerRole?: string): Bubb
return groups
}
+
+// 按对话轮次分组:
+// - 以 user 消息作为一轮对话的开始
+// - 将后续的 ai/system 消息归入同一组,直到下一条 user 出现
+const groupByTurn = (msgs: BubbleMessage[]): BubbleMessageGroup[] => {
+ const groups: BubbleMessageGroup[] = []
+ let currentGroup: BubbleMessageGroup | null = null
+
+ msgs.forEach((message, index) => {
+ const role = message.role || 'assistant'
+
+ if (role === 'user') {
+ // 遇到新的 user,开启新一轮对话
+ currentGroup = {
+ role,
+ messages: [message],
+ messageIndexes: [index],
+ startIndex: index,
+ }
+ groups.push(currentGroup)
+ } else if (currentGroup) {
+ // 将 ai/system 等回复归入当前轮次
+ currentGroup.messages.push(message)
+ currentGroup.messageIndexes.push(index)
+ } else {
+ // 没有 user 作为起点时,单独成组兜底
+ const fallbackGroup: BubbleMessageGroup = {
+ role,
+ messages: [message],
+ messageIndexes: [index],
+ startIndex: index,
+ }
+ groups.push(fallbackGroup)
+ currentGroup = fallbackGroup
+ }
+ })
+
+ return groups
+}
+
+// 统一对外暴露的分组函数,根据 activeMode 切换具体实现
+const customGroupStrategy = (msgs: BubbleMessage[]): BubbleMessageGroup[] => {
+ if (activeMode.value === 'turn') {
+ return groupByTurn(msgs)
+ }
+ return groupByTime(msgs)
+}
+
+const activeButtonStyle: Record = {
+ backgroundColor: '#409eff',
+ color: '#fff',
+ border: '1px solid #409eff',
+ borderRadius: '4px',
+}
+
+const inactiveButtonStyle: Record = {
+ backgroundColor: '#fff',
+ color: '#666',
+ border: '1px solid #ddd',
+ borderRadius: '4px',
+}