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 @@ @@ -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 @@ @@ -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', +}