-
Notifications
You must be signed in to change notification settings - Fork 7
refactor(bubble): refactor bubble components for 0.4.0 version #266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…enhanced message handling and rendering
… rendering and enhance message handling
…message rendering and context management
…ndering and integrate jsonrepair for argument handling
…or renderer matching and enhance loading state handling
… new wrapper components and remove deprecated renderers
WalkthroughRefactors the Bubble system to a message-group and composable-based architecture: introduces BubbleMessage model, grouping strategies, new wrapper components and composables for renderer resolution and state, replaces legacy renderers with Box/Text fallbacks, updates public API and docs, and adjusts packaging/dependencies. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant BubbleList
participant Grouping
participant BubbleItem
participant Bubble
participant BoxResolver
participant ContentResolver
participant Renderer
Client->>BubbleList: provide messages[]
BubbleList->>Grouping: apply groupStrategy
Grouping-->>BubbleList: messageGroups[]
BubbleList->>BubbleItem: render group
BubbleItem->>Bubble: render messageGroup
Bubble->>BoxResolver: useBubbleBoxRenderer(messages, contentIndex?)
BoxResolver-->>Bubble: box component + attrs
Bubble->>ContentResolver: useBubbleContentRenderer(message, contentIndex)
ContentResolver-->>Bubble: content component
Bubble->>Renderer: render component(s)
Renderer-->>Client: rendered DOM
Note right of Bubble: state-change events emitted from BubbleContentWrapper and bubble bubble-level emit
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… improved rendering capabilities
…t and improved prop handling for polymorphic messages
…d slot handling and message grouping
… enhanced content safety
…p, including demo components and routing
…ing for improved message handling
…ne list structure for improved message rendering
…d improve Bubble component logic
…in Reasoning and Tools components
…up the project structure
📦 Package Previewpnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot@bc3666c pnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot-kit@bc3666c pnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot-svgs@bc3666c commit: bc3666c |
… structure for improved message handling and rendering
…n rendering support, and update demo examples
…erer definitions for improved performance
…consistency and clarity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (8)
docs/.vitepress/theme/style.css (1)
45-53: Consider using a more stable selector for the Bubble demo styles.The selector
div:has(h1[id='bubble-气泡组件'])relies on a specific heading ID that may change if documentation is updated or localized. Additionally, nested CSS syntax (.vitepress-demo-plugin-previewinside the:has()block) requires CSS nesting support.Consider adding a dedicated class to the Bubble demo container for more maintainable targeting.
packages/components/src/bubble/utils.ts (1)
13-29: Consider caching failed import attempts.When the dynamic imports fail,
markdownItAndDompurifyis set tonull. Sincenullis falsy, the check on line 16 won't short-circuit subsequent calls, causing the import to be retried and the warning to be logged repeatedly.Consider using a sentinel value or a separate flag to track failed attempts:
🔎 Proposed fix to prevent retry spam
-let markdownItAndDompurify: { markdown: typeof MarkdownIt; dompurify: typeof DOMPurify } | null = null +let markdownItAndDompurify: { markdown: typeof MarkdownIt; dompurify: typeof DOMPurify } | null | false = null export const getMarkdownItAndDompurify = async () => { + if (markdownItAndDompurify === false) { + return null + } if (markdownItAndDompurify) { return markdownItAndDompurify } try { const [md, dompurify] = await Promise.all([import('markdown-it'), import('dompurify')]) markdownItAndDompurify = { markdown: md.default, dompurify: dompurify.default } return markdownItAndDompurify } catch { console.warn('[BubbleMarkdownRenderer] install markdown-it and dompurify to use markdown renderer') - markdownItAndDompurify = null + markdownItAndDompurify = false return null } }packages/components/src/bubble/composables/useMessageContent.ts (1)
9-22: Consider documenting the generic type parameter constraints.The generic type
Tis asserted on line 21 without runtime validation. While this provides flexibility, incorrect usage (e.g.,useMessageContent<number>(...)when content is actually a string) will fail silently at runtime. Consider adding JSDoc to clarify the caller's responsibility for type safety, or constrainingTto valid content types.📝 Suggested documentation enhancement
+/** + * Resolves message content reactively with optional array indexing. + * @template T - Expected content type (caller must ensure this matches actual content type) + * @param message - The bubble message (ref, getter, or value) + * @param contentIndex - Optional index for array content (defaults to 0) + * @returns Computed ref with resolved content + */ export const useMessageContent = <T = string | ChatMessageContentItem | undefined>( message: MaybeRefOrGetter<BubbleMessage>, contentIndex?: number, ) => {packages/components/src/bubble/composables/useBubbleStore.ts (1)
31-33: Consider warning when store is not provided.The function returns an empty object when
BUBBLE_STORE_KEYis not provided (line 32), which could mask integration errors where consumers expect a store but forgot to callsetupBubbleStore. Consider either:
- Adding a development-mode warning when the default is used
- Documenting that consumers must call
setupBubbleStorebefore usinguseBubbleStore💡 Option 1: Add development warning
export function useBubbleStore<T extends Record<string, unknown> = Record<string, unknown>>(): T { - return inject(BUBBLE_STORE_KEY, {}) as T + const store = inject(BUBBLE_STORE_KEY, undefined) + if (!store && process.env.NODE_ENV !== 'production') { + console.warn('[useBubbleStore] Store not provided. Did you forget to call setupBubbleStore?') + } + return (store || {}) as T }packages/components/src/bubble/composables/useOmitMessageFields.ts (2)
17-20: Potential type safety issue with Object.entries/fromEntries.The
Object.entries()andObject.fromEntries()pattern (lines 18-19) will lose non-enumerable properties, getters, and prototype chain information from the original message object. The type assertionas Omit<BubbleMessage, K>assumes the filtered result matches the expected type, but this isn't verified at runtime. IfBubbleMessagecontains non-plain properties, this could cause subtle bugs.Consider using a more type-safe approach with object destructuring if the omitted fields are known statically, or document that this utility only works with plain message objects.
22-27: Consider performance implications of computed object spread.The
restPropscomputed ref creates a new object via spread operator on every access (lines 23-25). If this composable is used in frequently re-rendering components, consider whether the computed ref is necessary or if the operation could be deferred/memoized differently.This is likely acceptable for most use cases, but worth noting if performance profiling reveals issues.
packages/components/src/bubble/BubbleList.vue (1)
23-24: Consider using a more idiomatic pattern for conditional function assignment.The
scrollToBottomFnis initialized as an empty async function and conditionally reassigned. This works but theletwith conditional reassignment could be slightly confusing.🔎 Alternative approach using a ref
-let scrollToBottomFn: (behavior?: ScrollBehavior) => Promise<void> = async () => {} +const scrollToBottomFn = ref<(behavior?: ScrollBehavior) => Promise<void>>(async () => {}) if (props.autoScroll) { // ... - scrollToBottomFn = scrollToBottom + scrollToBottomFn.value = scrollToBottom } defineExpose({ - scrollToBottom: scrollToBottomFn, + scrollToBottom: (...args: Parameters<typeof scrollToBottomFn.value>) => scrollToBottomFn.value(...args), })However, based on learnings, the autoScroll prop setup is intentionally non-reactive and only initialized once, so the current approach is acceptable.
packages/components/src/bubble/Bubble.vue (1)
74-91: Split mode rendering iterates over resolved content correctly.The loop iterates using
resolveMessageContent(messages[0])which is called twice (once inv-for, once potentially inshouldSplitcomputed). Consider caching this value.🔎 Cache resolved content for split mode
Add a computed property to avoid redundant calls:
const splitContent = computed(() => { if (shouldSplit.value) { return resolveMessageContent(messages.value[0]) as unknown[] } return [] })Then use in template:
- <BubbleBoxWrapper - v-for="(_, index) in resolveMessageContent(messages[0])" + <BubbleBoxWrapper + v-for="(_, index) in splitContent"
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
packages/svgs/src/assets/atom.svgis excluded by!**/*.svg
📒 Files selected for processing (46)
docs/.vitepress/theme/style.cssdocs/demos/examples/Assistant.vuedocs/demos/tools/conversation/Basic.vuedocs/demos/tools/message/Basic.vuedocs/package.jsondocs/src/components/bubble.mdpackages/components/package.jsonpackages/components/src/bubble/Bubble.vuepackages/components/src/bubble/BubbleBoxWrapper.vuepackages/components/src/bubble/BubbleContentWrapper.vuepackages/components/src/bubble/BubbleItem.vuepackages/components/src/bubble/BubbleList.vuepackages/components/src/bubble/BubbleProvider.vuepackages/components/src/bubble/components/ContentItem.vuepackages/components/src/bubble/components/index.tspackages/components/src/bubble/composables/index.tspackages/components/src/bubble/composables/useBubbleBoxRenderer.tspackages/components/src/bubble/composables/useBubbleContentRenderer.tspackages/components/src/bubble/composables/useBubbleStateChange.tspackages/components/src/bubble/composables/useBubbleStore.tspackages/components/src/bubble/composables/useMessageContent.tspackages/components/src/bubble/composables/useMessageGroup.tspackages/components/src/bubble/composables/useOmitMessageFields.tspackages/components/src/bubble/constants.tspackages/components/src/bubble/index.tspackages/components/src/bubble/index.type.tspackages/components/src/bubble/renderers/Box.vuepackages/components/src/bubble/renderers/Text.vuepackages/components/src/bubble/renderers/allRenderers.tspackages/components/src/bubble/renderers/class-renderer.tspackages/components/src/bubble/renderers/collapsible-text.tspackages/components/src/bubble/renderers/collapsible-text.vuepackages/components/src/bubble/renderers/defaultRendererMap.tspackages/components/src/bubble/renderers/defaultRenderers.tspackages/components/src/bubble/renderers/index.tspackages/components/src/bubble/renderers/index.type.tspackages/components/src/bubble/renderers/markdown.tspackages/components/src/bubble/renderers/text.tspackages/components/src/bubble/renderers/text.vuepackages/components/src/bubble/renderers/tool.tspackages/components/src/bubble/renderers/tool.vuepackages/components/src/bubble/utils.tspackages/components/src/index.tspackages/components/src/styles/animations.csspackages/components/src/styles/components/bubble.lesspackages/components/vite.config.ts
💤 Files with no reviewable changes (14)
- packages/components/src/bubble/renderers/collapsible-text.ts
- packages/components/src/bubble/renderers/class-renderer.ts
- packages/components/src/bubble/renderers/index.ts
- packages/components/src/bubble/renderers/markdown.ts
- packages/components/src/bubble/renderers/index.type.ts
- packages/components/src/bubble/renderers/tool.ts
- packages/components/src/bubble/components/index.ts
- packages/components/src/bubble/renderers/defaultRendererMap.ts
- packages/components/src/bubble/renderers/text.ts
- packages/components/src/bubble/renderers/collapsible-text.vue
- packages/components/src/bubble/components/ContentItem.vue
- docs/src/components/bubble.md
- packages/components/src/bubble/renderers/tool.vue
- packages/components/src/bubble/renderers/text.vue
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 123
File: packages/components/src/bubble/message/Message.vue:22-40
Timestamp: 2025-06-25T07:04:18.791Z
Learning: In the Message component (packages/components/src/bubble/message/Message.vue), the renderer resolution is intentionally not reactive to type changes - the component is designed to work with a fixed type that doesn't change after initialization.
📚 Learning: 2025-06-25T07:04:18.791Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 123
File: packages/components/src/bubble/message/Message.vue:22-40
Timestamp: 2025-06-25T07:04:18.791Z
Learning: In the Message component (packages/components/src/bubble/message/Message.vue), the renderer resolution is intentionally not reactive to type changes - the component is designed to work with a fixed type that doesn't change after initialization.
Applied to files:
packages/components/src/bubble/renderers/Box.vuepackages/components/src/bubble/composables/useMessageGroup.tspackages/components/src/bubble/index.tspackages/components/src/bubble/renderers/allRenderers.tspackages/components/src/bubble/composables/useBubbleContentRenderer.tsdocs/demos/tools/conversation/Basic.vuedocs/demos/tools/message/Basic.vuepackages/components/src/bubble/BubbleItem.vuepackages/components/src/bubble/constants.tspackages/components/src/bubble/composables/useBubbleBoxRenderer.tspackages/components/src/bubble/composables/useMessageContent.tspackages/components/src/bubble/BubbleBoxWrapper.vuepackages/components/src/bubble/BubbleList.vuepackages/components/src/bubble/Bubble.vuepackages/components/src/bubble/index.type.tspackages/components/src/bubble/renderers/defaultRenderers.tspackages/components/src/bubble/renderers/Text.vuepackages/components/src/bubble/BubbleProvider.vuepackages/components/src/bubble/BubbleContentWrapper.vue
📚 Learning: 2025-06-18T09:29:47.974Z
Learnt from: SonyLeo
Repo: opentiny/tiny-robot PR: 119
File: packages/components/src/attachments/index.less:213-213
Timestamp: 2025-06-18T09:29:47.974Z
Learning: 在 packages/components/src/attachments/index.less 中,.tr-file-card__close 的背景色使用了硬编码的 rgb(194, 194, 194),但这个UI元素(关闭按钮)将会被直接替换为图标,所以不需要抽取为CSS变量。
Applied to files:
packages/components/src/styles/components/bubble.lesspackages/components/src/bubble/Bubble.vuedocs/.vitepress/theme/style.css
📚 Learning: 2025-12-29T09:18:14.040Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 270
File: packages/components/src/bubble/BubbleList.vue:42-60
Timestamp: 2025-12-29T09:18:14.040Z
Learning: In packages/components/src/bubble/BubbleList.vue, the autoScroll prop setup is intentionally non-reactive - it only needs to be initialized once at component creation and does not need to respond to prop changes during the component lifecycle.
Applied to files:
packages/components/src/bubble/BubbleList.vue
🧬 Code graph analysis (7)
packages/components/src/bubble/composables/useMessageGroup.ts (2)
packages/components/src/bubble/index.type.ts (1)
BubbleMessageGroup(57-62)packages/components/src/bubble/constants.ts (1)
BUBBLE_MESSAGE_GROUP_KEY(8-9)
packages/components/src/bubble/composables/useBubbleStore.ts (1)
packages/components/src/bubble/constants.ts (1)
BUBBLE_STORE_KEY(36-36)
packages/components/src/bubble/renderers/allRenderers.ts (2)
packages/components/src/index.ts (1)
BubbleRenderers(42-42)packages/components/src/bubble/index.ts (1)
BubbleRenderers(46-46)
packages/components/src/bubble/composables/useOmitMessageFields.ts (1)
packages/components/src/bubble/index.type.ts (2)
BubbleContentRendererProps(80-86)BubbleMessage(38-45)
packages/components/src/bubble/composables/useBubbleStateChange.ts (3)
packages/components/src/bubble/constants.ts (1)
BUBBLE_STATE_CHANGE_FN_KEY(38-39)packages/components/src/index.ts (1)
useBubbleStateChangeFn(45-45)packages/components/src/bubble/index.ts (1)
useBubbleStateChangeFn(41-41)
packages/components/src/bubble/index.type.ts (1)
packages/kit/src/types.ts (1)
ChatMessage(11-16)
packages/components/src/bubble/renderers/defaultRenderers.ts (1)
packages/components/src/bubble/index.type.ts (2)
BubbleBoxRendererMatch(64-69)BubbleContentRendererMatch(71-76)
🔇 Additional comments (44)
packages/components/src/styles/animations.css (1)
6-31: LGTM! Well-implemented utility animations.The three new animations (ping, pulse, bounce) are cleanly implemented using performant properties (transform and opacity). The bounce animation's cubic-bezier timing functions create smooth, natural motion. These utility animations follow common conventions and will work well for loading states, notifications, and interactive feedback in the bubble components.
docs/demos/examples/Assistant.vue (1)
54-55: LGTM! API surface updated correctly.The prop names have been correctly updated to align with the new TrBubbleList API:
items→messagesandroles→role-configs. The bindings remain semantically correct.docs/demos/tools/conversation/Basic.vue (1)
3-3: LGTM! Consistent API update.The TrBubbleList props have been updated consistently with the new API surface, matching the changes across other demo files.
docs/package.json (1)
26-27: LGTM!The addition of
dompurifyandmarkdown-itas direct dependencies in the docs project correctly satisfies the peer dependency requirements from@opentiny/tiny-robotcomponents package. Version ranges are consistent across packages.packages/components/vite.config.ts (1)
46-46: LGTM!Adding
markdown-itanddompurifyto the external list correctly aligns with their move topeerDependencies, ensuring these packages are not bundled and must be provided by consumers.docs/.vitepress/theme/style.css (1)
91-122: LGTM!The color value normalization and formatting adjustments improve style consistency without changing behavior.
packages/components/src/styles/components/bubble.less (1)
1-48: LGTM!The CSS variable reorganization is well-structured with clear categorization. The self-referential pattern on line 24 (
box-image-border: 4px solid var(--tr-bubble-box-image-border-color)) works correctly since all variables are generated in the same:rootscope.packages/components/src/bubble/composables/useBubbleStateChange.ts (1)
1-12: LGTM!Clean implementation following Vue 3 provide/inject patterns. The fallback warning function in
useBubbleStateChangeFnprovides graceful degradation when the provider is not set up, which is appropriate for optional state management scenarios.packages/components/src/bubble/constants.ts (1)
1-56: LGTM!Well-structured constants module with:
- Properly typed
InjectionKeyconstants usingSymbol()for collision-free injectionMaybeRefOrGettertyping enabling reactive flexibilityBubbleRendererMatchPrioritywith strategic gaps (0, 10, 20) allowing future intermediate priorities- Clear bilingual documentation
packages/components/src/bubble/index.type.ts (3)
64-76: LGTM!The asymmetric signatures are intentional and correct:
BubbleBoxRendererMatch.find(messages[])- box renderer wraps message groupsBubbleContentRendererMatch.find(message)- content renderer handles individual messagesThis aligns well with the message-group architecture.
38-45: LGTM!The generic
BubbleMessage<T, S>type provides good flexibility for typed content (T extends ChatMessageContent) and custom state (S extends Record<string, unknown>), enabling type-safe usage while maintaining OpenAI format compatibility.
22-32: No action needed—this divergence is intentional and already documented.The
ChatMessagein bubble's index.type.ts is an internal interface (not exported) designed specifically for OpenAI-compatible message handling, as indicated by the existing comment "聊天消息接口(支持 OpenAI 格式)". It serves only to define the internal structure ofBubbleMessage, which is the actual exported type. This is a separate, purpose-built type hierarchy from kit's more generalChatMessage, and the code already clarifies this distinction through its documentation.packages/components/package.json (2)
19-23: LGTM!Moving
dompurifyandmarkdown-ittopeerDependenciesis the correct approach for this refactor - it allows consumers to control these dependency versions and enables tree-shaking when not used. ThedevDependenciesentries ensure local development works correctly.
28-28: jsonrepair is actively used in the codebase.The dependency is lazily imported via dynamic import in
packages/components/src/bubble/utils.tsfor bubble component functionality.packages/components/src/bubble/renderers/allRenderers.ts (1)
1-7: LGTM! Clean barrel export.The structure is straightforward and aligns well with the composable-based rendering architecture. The consolidated
BubbleRenderersexport provides a clear public API surface for the default renderer components.packages/components/src/bubble/utils.ts (1)
4-11: LGTM! Proper lazy-loading with caching.The caching pattern correctly ensures that
jsonrepairis loaded only once, and subsequent calls return the cached promise.packages/components/src/bubble/renderers/Box.vue (3)
1-5: LGTM! Props definition follows the intended pattern.The component correctly defines props via
BubbleBoxRendererPropstype. Per the design (based on retrieved learnings), renderer components work with fixed types and don't need reactive prop handling.
7-11: LGTM! Clean template structure.The template uses data attributes for styling variants and provides a slot for flexible content composition.
13-43: LGTM! Comprehensive theming with CSS variables.The styling approach uses CSS variables for all themeable properties and provides multiple variants (shape, placement, box-type) through data attributes. The logic for
cornershape inheriting fromroundedand then selectively overriding corners is appropriate.packages/components/src/bubble/renderers/Text.vue (3)
1-8: LGTM! Proper composable usage.The component correctly uses
useMessageContentwith a getter function for reactive message access and passescontentIndexdirectly.
10-14: LGTM! Template handles both string and object content.The conditional rendering and type checking logic correctly handles content that may be either a string or an object with a
textproperty. The optional chaining ensures safety even if the object structure varies.
16-32: LGTM! Well-structured text styling.The styling correctly handles text presentation with proper whitespace preservation (
pre-wrap), word breaking, and spacing between consecutive text blocks.packages/components/src/bubble/BubbleContentWrapper.vue (2)
1-22: LGTM! Well-structured event handling and renderer resolution.The component correctly:
- Resolves the content renderer via
useBubbleContentRenderer- Defines typed emits for state changes
- Wraps state changes with
contentIndexcontext- Establishes the state change provider via
setupBubbleStateChangeFn
24-26: LGTM! Clean dynamic component rendering.The template correctly renders the resolved renderer component and forwards all props via
v-bind.packages/components/src/bubble/index.ts (1)
38-46: LGTM! Public API reorganized for composable-based architecture.The new exports shift from direct renderer classes to composables and a consolidated
BubbleRenderersobject. This aligns with the PR objectives for the v0.4.0 refactor and provides a cleaner, more flexible API surface.Note: This is a breaking change from the previous API, but this is clearly intentional for the major version update.
packages/components/src/bubble/composables/useMessageGroup.ts (2)
5-7: LGTM! Clean provider setup.The function correctly establishes the message group provider using Vue's
provideAPI with the appropriate injection key.
9-11: LGTM! Proper injection with sensible default.The function correctly injects the message group with
undefinedas the default, which is appropriate for optional context that may not always be provided.packages/components/src/bubble/composables/index.ts (1)
1-7: LGTM! Clean composables barrel export.The barrel file properly aggregates all bubble composables, providing a clean single import point for consumers of the bubble API.
packages/components/src/bubble/BubbleItem.vue (1)
1-42: LGTM!The component properly establishes reactive message group context via
setupBubbleMessageGroupand cleanly forwards slots and events to the wrappedBubblecomponent. The state-change event delegation pattern is well-implemented.packages/components/src/bubble/renderers/defaultRenderers.ts (1)
1-11: LGTM!The use of
markRawon lines 10-11 is appropriate for preventing unnecessary reactivity on component definitions, which can improve performance. The empty default match arrays provide a clean foundation for the composable-based renderer system.packages/components/src/bubble/BubbleBoxWrapper.vue (1)
12-12: ThecontentIndexparameter is intentionally non-reactive. Component instances with specificcontentIndexvalues are created and destroyed via thev-forloop in the parent component;contentIndexdoes not change within a single instance's lifecycle. This pattern is consistent acrossuseBubbleBoxRenderer,useBubbleContentRenderer, anduseMessageContent, indicating it is the intended design.Likely an incorrect or invalid review comment.
packages/components/src/bubble/composables/useBubbleBoxRenderer.ts (3)
11-22: LGTM! Clean provider setup with conditional injection.The conditional
providecalls ensure that only defined values are injected, avoiding unnecessary overrides of parent providers. This aligns well with the composable-driven architecture.
28-35: Well-documented prop-level fallback mechanism.The JSDoc clearly explains the purpose - providing prop-level configuration that won't override parent provider. This separation of concerns between provider-level and prop-level configuration is a good pattern.
37-64: Solid renderer resolution with clear fallback priority.The resolution logic is clean:
- Match against provided/default renderer matches
- Fall back through prop-level → provider-level → default
One minor observation:
toValue(fallbackBoxRenderer)on line 62 could returnundefinedwhen injected withundefinedas the default (line 45), but this is correctly handled by the||chain.Based on learnings, the non-reactive resolution approach aligns with the intentional design where renderer resolution doesn't need to respond to type changes after initialization.
packages/components/src/bubble/BubbleList.vue (3)
95-131: Divider grouping logic looks correct but has subtle role assignment behavior.On line 121, when a message is not a divider, the group role is set to
message.role || ''. However, this means non-divider messages with different roles could be grouped together if they appear consecutively after a divider.For example:
[{role: 'user'}, {role: 'assistant'}, {role: 'system'}]withdividerRole: 'user'would group assistant and system messages together with role = 'assistant'.Is this the intended behavior? The comment suggests grouping by "divider/non-divider type" which would make this correct, but it may be worth documenting this behavior explicitly.
52-87: Good grouping logic with proper sealing mechanism for array content.The
isLastGroupSealedflag correctly prevents subsequent messages from being added to a group when the previous message had array content. The Chinese comments are helpful for understanding the business logic.The edge case of undefined or empty role strings is properly handled: when
message.roleisundefinedor empty (defaulting to''), the downstreamBubbleItemcomponent receives the fallback role (group.role || props.fallbackRole) with a default value of'assistant', ensuring consumers never encounter empty role strings.
168-168: The messageIndex calculation is correct. The Bubble component emitsmessageIndexas a zero-based offset within the group's message array, and BubbleList correctly converts it to an absolute index by addinggroup.startIndex. No changes needed.packages/components/src/bubble/Bubble.vue (4)
29-40: Good fallback pattern for standalone vs. nested usage.The component correctly supports both:
- Nested usage (messages from parent BubbleItem via injection)
- Standalone usage (message constructed from individual props)
The computed
messagesarray ensures a consistent interface for rendering.
43-44: Prop-level renderer setup uses reactive getters correctly.The arrow functions
() => props.fallbackBoxRendererensure reactivity is maintained when props change. This is the correct pattern for passing props to setup functions that expectMaybeRefOrGetter.
96-104: Nested template loops handle array content within single mode.The logic correctly handles messages where content is an array by rendering multiple
BubbleContentWrapperinstances. The key structurecontent-${contentIndex}is appropriate here since it's scoped within each message.
177-187: CSS module for avatar positioning is well-structured.Using a CSS module for the avatar grid positioning separates concerns between scoped layout styles and module-based component-specific styles. The placement-based grid positioning (
grid-area) cleanly handles LTR/RTL-like layouts.packages/components/src/bubble/BubbleProvider.vue (3)
16-26: Priority sorting places lower values first - verify this is intentional.The sort
(a.priority ?? 0) - (b.priority ?? 0)results in ascending order, meaning renderers with lower priority values are checked first. This could be counter-intuitive if "higher priority" is expected to mean "checked first."Typically, priority systems use either:
- Higher number = higher priority (descending sort needed)
- Lower number = higher priority (current ascending sort)
Please verify the intended priority semantics match the implementation. If higher numbers should take precedence:
🔎 Potential fix for descending priority
- .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)) + .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
28-37: Clean fallback resolution and setup hook invocation.The computed properties correctly provide default values when props are undefined, and the setup hooks are called with reactive computed refs ensuring the injected values stay in sync with prop changes.
12-14: Consider adding default values for optional props.Unlike
BubbleList.vuewhich useswithDefaults, this component uses plaindefineProps<BubbleProviderProps>(). IfBubbleProviderPropshas optional fields without defaults, TypeScript will infer them as potentiallyundefined.The current implementation handles this correctly via
props.boxRendererMatches || []patterns, so this is just a stylistic observation.
… 'items' and 'roles' to 'messages' and 'role-configs' for improved clarity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
docs/demos/tools/conversation/IndexedDB.vue (1)
34-43: Consider renaming the variable for clarity.The variable is named
roleswhile the prop is nowrole-configs. For better alignment with the new API, consider renaming:-const roles: Record<string, BubbleRoleConfig> = { +const roleConfigs: Record<string, BubbleRoleConfig> = { assistant: { placement: 'start', avatar: aiAvatar, }, user: { placement: 'end', avatar: userAvatar, }, }And update the template binding:
-<tr-bubble-list :messages="messages" :role-configs="roles"></tr-bubble-list> +<tr-bubble-list :messages="messages" :role-configs="roleConfigs"></tr-bubble-list>Note: The removal of
maxWidth: '80%'from both role configurations appears intentional as part of the v0.4.0 refactor. Verify that bubble width constraints are now handled by the component's internal defaults or other configuration.docs/demos/tools/conversation/LocalStorage.vue (1)
34-43: Same observations as IndexedDB.vue.The role configuration follows the same pattern with
maxWidthremoved and variable namedroleswhile the prop isrole-configs. Consider the same optional refactor suggested in IndexedDB.vue for consistency and clarity.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
docs/demos/tools/conversation/IndexedDB.vuedocs/demos/tools/conversation/LocalStorage.vue
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 123
File: packages/components/src/bubble/message/Message.vue:22-40
Timestamp: 2025-06-25T07:04:18.791Z
Learning: In the Message component (packages/components/src/bubble/message/Message.vue), the renderer resolution is intentionally not reactive to type changes - the component is designed to work with a fixed type that doesn't change after initialization.
📚 Learning: 2025-06-25T07:04:18.791Z
Learnt from: gene9831
Repo: opentiny/tiny-robot PR: 123
File: packages/components/src/bubble/message/Message.vue:22-40
Timestamp: 2025-06-25T07:04:18.791Z
Learning: In the Message component (packages/components/src/bubble/message/Message.vue), the renderer resolution is intentionally not reactive to type changes - the component is designed to work with a fixed type that doesn't change after initialization.
Applied to files:
docs/demos/tools/conversation/IndexedDB.vuedocs/demos/tools/conversation/LocalStorage.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build / build
🔇 Additional comments (2)
docs/demos/tools/conversation/IndexedDB.vue (1)
3-3: LGTM! API migration correctly implemented.The prop bindings have been successfully updated to use the new API surface (
:messagesand:role-configs), aligning with the v0.4.0 bubble component refactor.docs/demos/tools/conversation/LocalStorage.vue (1)
3-3: LGTM! Consistent API migration.The prop bindings correctly use the new API surface, matching the implementation in IndexedDB.vue.
🧹 Preview Cleaned UpThe preview deployment has been removed. |

Bubble 组件重构 PR 描述
📋 概述
本次 PR 对
packages/components/src/bubble目录进行了大规模重构,从基于简单 props 的组件架构重构为基于消息分组和渲染器匹配的现代化架构。重构涉及核心组件重写、渲染器系统重构、类型系统优化以及新增多项功能特性。🎯 主要变更
1. 核心组件重构
1.1 BubbleList.vue - 完全重写
变更前:
itemsprop 的简单列表渲染@vueuse/core的useScroll实现自动滚动Bubble组件roles配置和loading状态变更后:
messagesprop 的消息列表consecutive: 连续相同角色的消息合并为一组divider: 按分割角色分组(默认策略)useAutoScrollcomposable 实现自动滚动BubbleItem组件渲染消息分组groupStrategy、dividerRole、fallbackRole等配置项state-change事件,支持消息状态变更通知关键代码变更:
1.2 Bubble.vue - 架构重构
变更前:
contentprop 的直接渲染customContentField、abortedText、maxWidth等 propsContentItem组件渲染数组内容contentRendererprop 指定渲染器变更后:
messageGroup的渲染(通过 provide/inject)customContentField、abortedText等 propsBubbleBoxWrapper和BubbleContentWrapper组件contentRenderMode(single|split)控制渲染模式prefix、suffix、after、content-footerreasoning_content、tool_calls等 OpenAI 格式字段布局变更:
1.3 新增组件
BubbleItem.vue
BubbleList和Bubble之间的中间层messageGroup给子组件BubbleBoxWrapper.vue
useBubbleBoxRenderer选择匹配的容器渲染器BubbleContentWrapper.vue
useBubbleContentRenderer选择匹配的内容渲染器state-change事件2. 渲染器系统重构
2.1 渲染器架构变更
变更前:
Map<string, Renderer>的静态映射BubbleProvider的contentRenderersprop 配置ContentItem组件根据type字段匹配渲染器变更后:
find)的动态匹配系统priority)配置attributes)新的渲染器匹配接口:
2.2 渲染器文件变更
删除的文件:
renderers/class-renderer.ts- 类渲染器基类renderers/collapsible-text.ts和renderers/collapsible-text.vuerenderers/tool.ts和renderers/tool.vuerenderers/markdown.ts- Markdown 类渲染器renderers/text.ts和renderers/text.vue- 旧版文本渲染器renderers/defaultRendererMap.ts- 旧的渲染器映射renderers/index.ts和renderers/index.type.ts- 旧的导出文件新增的文件:
renderers/Box.vue- 基础容器渲染器renderers/Text.vue- 文本内容渲染器renderers/defaultRenderers.ts- 默认渲染器配置renderers/allRenderers.ts- 渲染器导出2.3 BubbleProvider.vue 重构
变更前:
contentRenderersprop 配置渲染器映射provide注入渲染器 Map 和 fallback 渲染器变更后:
boxRendererMatches和contentRendererMatches配置渲染器匹配规则fallbackBoxRenderer和fallbackContentRenderer配置 fallback 渲染器storeprop 提供全局状态3. Composables 系统
新增
composables/目录,将逻辑从组件中抽离:3.1 useBubbleBoxRenderer.ts
setupBubbleBoxRenderer和setupBubblePropBoxRenderer函数3.2 useBubbleContentRenderer.ts
setupBubbleContentRenderer和setupBubblePropContentRenderer函数3.3 useBubbleStore.ts
BubbleList和Bubble组件之间共享数据3.4 useBubbleStateChange.ts
3.5 useMessageContent.ts
state.content或content字段获取内容3.6 useMessageGroup.ts
BubbleItem和Bubble之间传递消息分组信息3.7 useOmitMessageFields.ts
4. 类型系统重构
4.1 消息类型变更
变更前:
变更后:
4.2 分组类型优化
新增
BubbleMessageGroup字段:messageIndexes: number[]- 记录消息在原始数组中的索引startIndex: number- 记录分组在原始数组中的起始索引isPolymorphic标记(不再需要)4.3 Props 接口重构
BubbleListProps 变更:
items→messages(类型从BubbleProps[]改为BubbleMessage[])roles、loading、loadingRolegroupStrategy、dividerRole、fallbackRole、roleConfigscontentRenderMode、autoScrollBubbleProps 变更:
BubbleMessage类型content、customContentField、abortedText、maxWidthcontentRenderMode、fallbackBoxRenderer、fallbackContentRenderershape支持'none'选项4.4 新增类型
BubbleBoxRendererMatch- 容器渲染器匹配规则BubbleContentRendererMatch- 内容渲染器匹配规则BubbleBoxRendererProps- 容器渲染器 propsBubbleContentRendererProps- 内容渲染器 propsBubbleRoleConfig- 角色配置类型BubbleListSlots- 列表插槽类型5. 工具函数
5.1 utils.ts 新增
新增
utils.ts文件,提供工具函数:getJsonrepair()- 动态导入 jsonrepair 库(用于工具调用参数修复)getMarkdownItAndDompurify()- 动态导入 markdown-it 和 DOMPurify(用于 Markdown 渲染)这些函数使用动态导入,避免在不需要时加载大型依赖。
6. 常量定义
6.1 constants.ts 新增
新增
constants.ts文件,定义所有注入键和常量:BUBBLE_MESSAGE_GROUP_KEY- 消息分组注入键BUBBLE_BOX_RENDERER_MATCHES_KEY- 容器渲染器匹配规则注入键BUBBLE_CONTENT_RENDERER_MATCHES_KEY- 内容渲染器匹配规则注入键BUBBLE_STORE_KEY- 全局状态注入键BUBBLE_STATE_CHANGE_FN_KEY- 状态变更函数注入键BubbleRendererMatchPriority- 渲染器匹配优先级常量7. 删除的文件和目录
7.1 组件文件
components/ContentItem.vue- 旧的内容项组件components/index.ts- 组件导出文件7.2 渲染器文件
renderers/class-renderer.tsrenderers/collapsible-text.ts和renderers/collapsible-text.vuerenderers/tool.ts和renderers/tool.vuerenderers/markdown.tsrenderers/text.ts和renderers/text.vuerenderers/defaultRendererMap.tsrenderers/index.ts和renderers/index.type.ts📊 变更统计
文件变更
主要新增文件
BubbleItem.vueBubbleBoxWrapper.vueBubbleContentWrapper.vueconstants.tsutils.tscomposables/目录(7 个文件)renderers/Box.vuerenderers/Text.vuerenderers/defaultRenderers.tsrenderers/allRenderers.ts主要删除文件
components/ContentItem.vuecomponents/index.tsrenderers/目录下的多个旧文件🔄 迁移指南
Props 变更
BubbleList:
Bubble:
渲染器配置变更
消息格式变更
🎨 设计改进
1. 关注点分离
2. 可扩展性提升
3. 类型安全
4. 性能优化
5. 功能增强
🧪 测试建议
1. 功能测试
2. 渲染器测试
3. 兼容性测试
BubbleList Props 变更:
items→messages(类型和结构都改变)roles→roleConfigs(结构改变)loading和loadingRolegroupStrategy、dividerRole、fallbackRoleBubble Props 变更:
contentprop(改为从BubbleMessage继承)customContentField、abortedText、maxWidthshape新增'none'选项渲染器系统:
类型系统:
BubbleProps结构完全改变BubbleProps改为BubbleMessage组件移除:
ContentItem组件已移除🚀 后续计划
Summary by CodeRabbit
Breaking Changes
maxWidthfrom role configs.New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.