Skip to content

Commit 801fd52

Browse files
committed
feat(bubble): refactor content rendering logic for improved flexibility
- Introduced `getContentItems` function to streamline content extraction from messages, enhancing the rendering process. - Updated `useBubbleBoxRenderer` and `useBubbleContentRenderer` to utilize the new content handling approach, ensuring consistent content structure. - Refactored rendering logic in `Bubble.vue` and related components to support unified content item format, improving compatibility with various content types. - Enhanced type definitions in `index.type.ts` to reflect changes in content handling, ensuring better type safety and clarity.
1 parent 9cdb943 commit 801fd52

7 files changed

Lines changed: 51 additions & 51 deletions

File tree

packages/components/src/bubble/Bubble.vue

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ const messages = computed(() => {
4444
return [{ role, content, reasoning_content, tool_calls, tool_call_id, name, id, loading, state }]
4545
})
4646
47+
const getContentItems = (message: BubbleMessage) => {
48+
const content = contentResolver(message)
49+
if (Array.isArray(content)) {
50+
return content
51+
}
52+
return [{ type: 'text', text: content || '' }]
53+
}
54+
4755
// Setup prop-level fallback renderers for responsive tracking
4856
setupBubblePropBoxRenderer({ fallbackBoxRenderer: () => props.fallbackBoxRenderer })
4957
setupBubblePropContentRenderer({ fallbackContentRenderer: () => props.fallbackContentRenderer })
@@ -88,6 +96,7 @@ if (!isInBubbleList) {
8896
:class="$style['tr-bubble__avatar']"
8997
/>
9098
<div class="tr-bubble__content">
99+
<!-- 在 contentRenderMode 为 split 情况下,如果一个气泡内只有一条消息,且 content 为数组,对每个 content 数组的项都渲染一个 box -->
91100
<template v-if="shouldSplitedContent">
92101
<BubbleBoxWrapper
93102
v-for="(_, index) in shouldSplitedContent"
@@ -110,21 +119,13 @@ if (!isInBubbleList) {
110119
<template v-else>
111120
<BubbleBoxWrapper :role="props.role" :placement="props.placement" :shape="props.shape" :messages="messages">
112121
<template v-for="(message, msgIndex) in messages" :key="`message-${msgIndex}`">
113-
<template v-if="Array.isArray(contentResolver(message))">
114-
<BubbleContentWrapper
115-
v-for="(_, contentIndex) in contentResolver(message)"
116-
:key="`content-${contentIndex}`"
117-
:message="message"
118-
:content-index="contentIndex"
119-
@state-change="emit('state-change', { ...$event, messageIndex: msgIndex })"
120-
></BubbleContentWrapper>
121-
</template>
122-
<template v-else>
123-
<BubbleContentWrapper
124-
:message="message"
125-
@state-change="emit('state-change', { ...$event, messageIndex: msgIndex })"
126-
></BubbleContentWrapper>
127-
</template>
122+
<BubbleContentWrapper
123+
v-for="(_, contentIndex) in getContentItems(message)"
124+
:key="`content-${contentIndex}`"
125+
:message="message"
126+
:content-index="contentIndex"
127+
@state-change="emit('state-change', { ...$event, messageIndex: msgIndex })"
128+
></BubbleContentWrapper>
128129
</template>
129130
<slot name="content-footer" :messages="messages" :role="props.role"></slot>
130131
</BubbleBoxWrapper>

packages/components/src/bubble/composables/useBubbleBoxRenderer.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
BUBBLE_BOX_PROP_FALLBACK_RENDERER_KEY,
66
BUBBLE_BOX_RENDERER_MATCHES_KEY,
77
} from '../constants'
8-
import type { BubbleBoxRendererMatch, BubbleMessage } from '../index.type'
8+
import type { BubbleBoxRendererMatch, BubbleMessage, ChatMessageContentItem } from '../index.type'
99
import { defaultBoxRendererMatches, defaultFallbackBoxRenderer } from '../renderers/defaultRenderers'
1010
import { useContentResolver } from './useContentResolver'
1111

@@ -47,15 +47,23 @@ export function useBubbleBoxRenderer(
4747
const propFallbackBoxRenderer = inject(BUBBLE_BOX_PROP_FALLBACK_RENDERER_KEY, undefined)
4848
const contentResolver = useContentResolver()
4949

50+
// 如果 contentIndex 为数字,说明是 split 模式,当前 messages 数组长度为 1
51+
if (typeof contentIndex === 'number') {
52+
if (toValue(messages).length !== 1) {
53+
throw new Error('[BubbleBoxRenderer] When contentIndex is a number, messages array length must be 1')
54+
}
55+
}
56+
5057
return computed(() => {
5158
const msgs = toValue(messages)
5259

60+
const resolvedContent = typeof contentIndex === 'number' ? contentResolver(msgs.at(0)!) : undefined
61+
const content = Array.isArray(resolvedContent)
62+
? (resolvedContent.at(contentIndex ?? 0) as ChatMessageContentItem)
63+
: { type: 'text', text: resolvedContent || '' }
64+
5365
const match = toValue(boxRendererMatches).find((match) =>
54-
match.find(
55-
msgs,
56-
msgs.map((msg) => contentResolver(msg)),
57-
contentIndex,
58-
),
66+
match.find(msgs, typeof contentIndex === 'number' ? content : undefined, contentIndex),
5967
)
6068

6169
if (match) {

packages/components/src/bubble/composables/useBubbleContentRenderer.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
BUBBLE_CONTENT_PROP_FALLBACK_RENDERER_KEY,
66
BUBBLE_CONTENT_RENDERER_MATCHES_KEY,
77
} from '../constants'
8-
import type { BubbleContentRendererMatch, BubbleMessage } from '../index.type'
8+
import type { BubbleContentRendererMatch, BubbleMessage, ChatMessageContentItem } from '../index.type'
99
import { defaultContentRendererMatches, defaultFallbackContentRenderer } from '../renderers/defaultRenderers'
1010
import { useContentResolver } from './useContentResolver'
1111

@@ -46,8 +46,11 @@ export function useBubbleContentRenderer(
4646

4747
return computed(() => {
4848
const msg = toValue(message)
49-
50-
const match = toValue(contentRendererMatches).find((match) => match.find(msg, contentResolver(msg), contentIndex))
49+
const resolvedContent = contentResolver(msg)
50+
const content = Array.isArray(resolvedContent)
51+
? (resolvedContent.at(contentIndex ?? 0) as ChatMessageContentItem)
52+
: { type: 'text', text: resolvedContent || '' }
53+
const match = toValue(contentRendererMatches).find((match) => match.find(msg, content, contentIndex))
5154
if (match) {
5255
return match.renderer
5356
}

packages/components/src/bubble/composables/useMessageContent.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,19 @@ export const useMessageContent = <T extends ChatMessageContent = ChatMessageCont
77
) => {
88
const contentResolver = useContentResolver()
99

10-
const content = computed(() => contentResolver(props.message))
11-
12-
const contentItem = computed(() => {
13-
const c = content.value
14-
return Array.isArray(c) ? (c.at(props.contentIndex ?? 0) as ChatMessageContentItem | undefined) : undefined
10+
const content = computed(() => {
11+
const resolvedContent = contentResolver(props.message)
12+
return Array.isArray(resolvedContent)
13+
? (resolvedContent.at(props.contentIndex ?? 0) as ChatMessageContentItem)
14+
: { type: 'text', text: resolvedContent || '' }
1515
})
1616

1717
const contentText = computed(() => {
18-
const c = content.value
19-
return Array.isArray(c) ? String(c.at(props.contentIndex ?? 0)?.text || '') : c || ''
18+
return content.value.type === 'text' ? String(content.value.text) : ''
2019
})
2120

2221
return {
2322
content,
24-
contentItem,
2523
contentText,
2624
}
2725
}

packages/components/src/bubble/index.type.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ export type BubbleBoxRendererMatch = {
6666
/**
6767
* 匹配函数,用于判断是否应该使用此渲染器
6868
* @param messages - 消息数组
69-
* @param resolvedContents - 每个消息经过 contentResolver 解析后的内容数组,与 messages 一一对应
70-
* @param contentIndex - 内容索引,用于指定要渲染的内容项(当 content 为数组时)
69+
* @param content - 要渲染的内容项。仅在 `split` 模式下(contentIndex 为数字)才会传入;为当前消息(messages[0])经过 `contentResolver` 解析后的内容;`messages[0].content` 一定是一个数组,`content` 则为对应索引的内容项,即 `messages[0].content[contentIndex]`;当 contentIndex 为 undefined 时,content 也为 undefined
70+
* @param contentIndex - 内容索引,用于指定要渲染的内容项。仅在 split 模式下才会传入(为数字),此时 messages 数组长度为 1
7171
* @returns 如果匹配则返回 true,否则返回 false
7272
*/
7373
find: (
7474
messages: BubbleMessage[],
75-
resolvedContents: BubbleMessage['content'][],
75+
content: ChatMessageContentItem | undefined,
7676
contentIndex: number | undefined,
7777
) => boolean
7878
renderer: Component<BubbleBoxRendererProps>
@@ -84,11 +84,11 @@ export type BubbleContentRendererMatch = {
8484
/**
8585
* 匹配函数,用于判断是否应该使用此渲染器
8686
* @param message - 消息对象
87-
* @param resolvedContent - 消息经过 contentResolver 解析后的内容
88-
* @param contentIndex - 内容索引,用于指定要渲染的内容项(当 content 为数组时)
87+
* @param content - 要渲染的内容项。为当前消息经过 contentResolver 解析并统一化后的内容项:若解析结果为数组,则取对应索引的内容项(由 contentIndex 指定);若为字符串,则转为 { type: 'text', text: string }。统一化为 ChatMessageContentItem 对象格式
88+
* @param contentIndex - 内容索引。由 contentResolver 的解析结果为数组时使用
8989
* @returns 如果匹配则返回 true,否则返回 false
9090
*/
91-
find: (message: BubbleMessage, resolvedContent: BubbleMessage['content'], contentIndex: number | undefined) => boolean
91+
find: (message: BubbleMessage, content: ChatMessageContentItem, contentIndex: number | undefined) => boolean
9292
renderer: Component<BubbleContentRendererProps>
9393
priority?: number
9494
attributes?: Record<string, string>

packages/components/src/bubble/renderers/Image.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const props = defineProps<BubbleContentRendererProps<ChatMessageContentItem[]>>(
88
const isLoaded = ref(false)
99
const hasError = ref(false)
1010
11-
const { contentItem: content } = useMessageContent(props)
11+
const { content } = useMessageContent(props)
1212
1313
const imageUrl = computed(() => {
1414
if (!content.value) {

packages/components/src/bubble/renderers/defaultRenderers.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,7 @@ import Tools from './Tools.vue'
1111

1212
export const defaultBoxRendererMatches: Array<BubbleBoxRendererMatch> = [
1313
{
14-
find: (_, contents, contentIndex) => {
15-
const content = contents.at(0)
16-
return (
17-
contents.length === 1 &&
18-
Array.isArray(content) &&
19-
typeof contentIndex === 'number' &&
20-
content[contentIndex].type === 'image_url'
21-
)
22-
},
14+
find: (_, content) => content?.type === 'image_url',
2315
renderer: markRaw(Box),
2416
priority: BubbleRendererMatchPriority.NORMAL,
2517
attributes: { 'data-box-type': 'image' },
@@ -43,9 +35,7 @@ export const defaultContentRendererMatches: Array<BubbleContentRendererMatch> =
4335
priority: BubbleRendererMatchPriority.NORMAL,
4436
},
4537
{
46-
find: (_, content, contentIndex) => {
47-
return Array.isArray(content) && typeof contentIndex === 'number' && content[contentIndex].type === 'image_url'
48-
},
38+
find: (_, content) => content.type === 'image_url',
4939
renderer: markRaw(Image),
5040
priority: BubbleRendererMatchPriority.CONTENT,
5141
},

0 commit comments

Comments
 (0)