-
Notifications
You must be signed in to change notification settings - Fork 7
refactor: components/kit 0.4.0 #286
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
… 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
… for improved functionality
… types for consistency
… ToolCall and BubbleChatMessageItem interfaces
… refactor props for improved flexibility
…ew event emission and state change handling
…s components for consistency in message handling
…essageFields composable and updating event emission in Bubble components
…w renderers and improving message handling with updated props and types
…nd enhance message handling with new props and plugins
…-change' across components and introduce BubbleRenderers for improved renderer management
…dex access with 'at()' method for better readability and safety
- Renamed `createSSEStreamGenerator` to `sseStreamToGenerator` for clarity. - Added comprehensive documentation in Chinese for the new function, detailing its purpose and usage. - Updated the export statement in `index.ts` to reflect the new function name.
- Updated demo content in `avatar-and-placement.vue` to reflect user and AI messages. - Added new demos for content rendering modes, including `content-render-mode.vue` and `content-resolver.vue`, showcasing single and split rendering. - Introduced custom renderer examples in `custom-renderer.vue` and `provider-renderer.vue` for enhanced flexibility in message display. - Implemented new grouping strategies in `list-consecutive.vue`, `list-custom-group.vue`, and `list-array-content.vue` to improve message organization. - Enhanced `loading.vue` and `streaming.vue` demos for better user interaction and visual feedback. - Updated documentation in `bubble.md` to include new features and usage examples.
- Added new sidebar items for '存储策略' and '工具函数' in the theme configuration. - Refactored demo components to utilize `activeConversationId` and `historyData` for improved state management. - Updated message handling in various demo files to reflect changes in the `useMessage` and `useConversation` composables. - Introduced new storage strategy demos for LocalStorage and IndexedDB, showcasing persistent conversation management. - Enhanced documentation for AI client and conversation management, marking deprecated features and providing clearer usage examples.
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. WalkthroughComprehensive v0.4.0 upgrade: introduces a two‑tier Bubble renderer architecture, plugin-based useMessage, per-conversation engines with storage strategies, streaming/tool-call support, many new renderers/composables, storage refactors, updated demos, and migration documentation. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Component as Bubble/BubbleList
participant Resolver as ContentResolver
participant Provider as BubbleProvider
participant Renderer as ContentRenderer
participant Store as BubbleStore
User->>Component: add/update message
Component->>Resolver: resolver(message)
Resolver-->>Component: resolved content
Component->>Provider: find matching renderer
Provider-->>Component: matched Renderer
Component->>Renderer: render(props)
Renderer->>Store: read/write state (open/liked/tool results)
Renderer-->>Component: emit state-change
Component-->>User: UI updated
sequenceDiagram
participant App
participant useMessage as useMessage Hook
participant Plugins as Plugin System
participant Provider as ResponseProvider
participant Engine as Message Engine
participant UI as App Component
App->>useMessage: initialize(options)
useMessage->>Plugins: load & deduplicate plugins
App->>useMessage: sendMessage(content)
useMessage->>Plugins: onTurnStart
useMessage->>Plugins: onBeforeRequest
useMessage->>Provider: POST request (with abortSignal)
Provider-->>useMessage: AsyncGenerator<ChatCompletion>
loop for each chunk
useMessage->>Plugins: onCompletionChunk(chunk)
useMessage->>Engine: merge delta into current message
useMessage->>UI: update messages
end
useMessage->>Plugins: onAfterRequest
useMessage->>Plugins: onTurnEnd
sequenceDiagram
participant App
participant useConversation as useConversation Hook
participant Storage as StorageStrategy
participant Engine as PerConversation Engine
participant AutoSave as ThrottleSaver
App->>useConversation: init(options)
useConversation->>Storage: loadConversations()
Storage-->>useConversation: ConversationInfo[]
App->>useConversation: switchConversation(id)
useConversation->>useConversation: ensureEngine(id)
useConversation->>Storage: loadMessages(id)
Storage-->>Engine: ChatMessage[]
useConversation->>AutoSave: setup throttle save(engine)
App->>Engine: sendMessage(content)
Engine-->>App: messages updated
AutoSave->>Storage: saveMessages(id, messages)
Storage-->>AutoSave: persisted
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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 |
📦 Package Previewpnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot@a694c23 pnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot-kit@a694c23 pnpm add https://pkg.pr.new/opentiny/tiny-robot/@opentiny/tiny-robot-svgs@a694c23 commit: a694c23 |
- Introduced migration guides for Bubble and Kit components, detailing changes from v0.3.x to v0.4.x. - Updated theme configuration to include a new sidebar item for the migration guide. - Enhanced demo examples with improved styling and updated properties for better user experience. - Refactored demo components to align with the latest API changes and best practices. - Added new CSS variables for consistent styling across components.
- 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.
- Updated the `state-change` event payload in `Bubble.vue`, `BubbleContentWrapper.vue`, `BubbleItem.vue`, and `BubbleList.vue` to make `contentIndex` a required field. - Adjusted type definitions in `index.type.ts` to reflect the change in `contentIndex` from optional to required. - Modified the `useBubbleContentRenderer` and related logic to ensure consistent handling of `contentIndex` across components.
…nderer - Introduced a new helper function `getContentAndIndex` to streamline the retrieval of content and index from messages. - Updated the main computed property to utilize the new function, enhancing readability and maintainability of the code. - Ensured consistent handling of content and index across the bubble rendering logic.
- Updated the grouping logic in `BubbleList.vue` to ensure that messages with an array content are only treated as independent groups if the message role is 'user'. - Adjusted the documentation in `index.type.ts` to reflect this new condition for message grouping, enhancing clarity on how messages are processed.
59486a0 to
4fb32c3
Compare
…ssage and useConversation - Added migration guides for `useMessage` and `useConversation`, detailing changes from v0.3.x to v0.4.x. - Updated theme configuration to include new sidebar items for the migration guides. - Enhanced documentation for AI client and conversation management, marking deprecated features and providing clearer usage examples. - Removed the obsolete `kit-migration.md` file to streamline documentation.
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: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/components/src/bubble/index.type.ts (1)
99-105: Update custom renderers:contentIndexis now required inBubbleContentRendererProps.The type definition now requires
contentIndex: number(previously optional). All custom renderers implementing this type must passcontentIndexexplicitly. If you were previously omitting this parameter or treating it as optional, update your renderer implementations to include it. Built-in renderers and wrapper components already reflect this change.Note: Documentation currently describes
contentIndexas optional, but the type definition enforces it as required. Align your custom renderers with the type definition.
🤖 Fix all issues with AI agents
In `@docs/demos/tools/conversation/Basic.vue`:
- Around line 128-130: The module-scope access to window when computing apiUrl
(the const apiUrl line referencing window.parent?.location.origin) will break
SSR/static builds; change the computation to first check for browser runtime
(e.g., typeof window !== 'undefined') before accessing window or window.parent,
and fall back to using import.meta/env BASE_URL or a safe origin when window is
not available; update the apiUrl definition (and related consts meta/baseUrl if
needed) so all window or location access is guarded by that runtime check.
In `@docs/demos/tools/conversation/LocalStorage.vue`:
- Line 49: The module-scope access to window.parent?.location.origin in the
apiUrl constant will throw during SSR; update the apiUrl initialization in
LocalStorage.vue so it only reads browser globals when available by guarding
with typeof window !== 'undefined' and using window.location (not
window.parent). Specifically, replace the top-level const apiUrl = ... with a
guarded expression (or compute apiUrl inside a setup/mounted hook) that returns
typeof window !== 'undefined' ? window.location.origin : '' (or another safe
server-side fallback) so server-side builds no longer reference window/location
at import time.
In `@docs/demos/tools/utils/SSEStream.vue`:
- Around line 45-55: The code currently reads parent origin directly and never
appends BASE_URL; change the apiUrl resolution to safely determine origin and
always append a normalized BASE_URL: use the existing meta and baseUrl variables
(ImportMetaEnv, ImportMetaWithEnv, meta, baseUrl) but compute origin with a
guarded check (if window.parent !== window) and a try/catch around accessing
window.parent.location.origin to avoid cross-origin throws, fall back to
window.location.origin on error, then normalize baseUrl to ensure it begins with
'/' (or empty string if not provided) and set apiUrl = origin +
normalizedBaseUrl so BASE_URL is always applied and parent access is safe.
In `@packages/components/src/bubble/composables/useBubbleBoxRenderer.ts`:
- Around line 63-65: The current assignment for content in
useBubbleBoxRenderer.ts unsafely casts resolvedContent.at(contentIndex ?? 0) to
ChatMessageContentItem; instead, check bounds and return undefined when out of
range: if resolvedContent is an array, compute idx = contentIndex ?? 0, then if
idx < 0 || idx >= resolvedContent.length set content = undefined, else set
content = resolvedContent[idx]; ensure the declared type for content remains
ChatMessageContentItem | undefined (no non-null cast) so downstream code (e.g.,
defaultRenderers.ts checks like content.type) must handle undefined safely.
In `@packages/components/src/bubble/composables/useCopyCleanup.ts`:
- Around line 7-35: The watcher in useCopyCleanup wrongly calls stopWatch()
inside its own callback causing it to stop on first run and also never removes
prior copy listeners; instead, keep the watch handle assigned to stopWatch (do
not call stopWatch() inside the callback), create a named handler (e.g.
handleCopy) that you add via elem.addEventListener('copy', handleCopy), and use
onCleanup within the watch callback to call elem.removeEventListener('copy',
handleCopy) so the previous listener is removed when elementRef changes or when
the watcher is stopped; ensure you still return/maintain stopWatch from watch
and let Vue's onCleanup manage per-run teardown.
In `@packages/components/src/bubble/renderers/Markdown.vue`:
- Around line 27-33: The code builds HTML with markdown(mdConfig ||
{}).render(content.value) and then calls dompurify.sanitize(...) but discards
the return value, leaving markdownContent.value set to unsanitized HTML; update
the watchEffect in the Markdown.vue block so you capture and assign the
sanitizer output to markdownContent.value (use the markdown and dompurify
variables from markdownItAndDompurify.value, call markdown.render(content.value)
into a temp var, pass that to dompurify.sanitize(..., dompurifyConfig), and
assign the sanitized result to markdownContent.value before rendering via
v-html).
In `@packages/components/src/bubble/renderers/Tool.vue`:
- Around line 45-67: The highlightJSON function injects raw JSON into HTML spans
without escaping HTML entities, allowing tool output to inject HTML/JS when
rendered via v-html; fix by HTML-escaping the input string first (convert & < >
" ' to entities) before running the regex/highlighting logic in highlightJSON
(or call a small escapeHtml helper at the start of highlightJSON), then continue
to apply the existing regex and class lookup (classes['number'], classes['key'],
classes['string'], etc.) so all injected content is safe when used with v-html.
- Around line 128-130: The expand-control (IconArrowDown with class
"expand-icon") is not keyboard-accessible; update the template in Tool.vue to
use a semantic, focusable control (preferably replace the IconArrowDown wrapper
div with a <button> or add tabindex="0", role="button", and keyboard handlers)
so keyboard users can toggle via Enter/Space and see state via ARIA. Ensure the
control calls the existing handleClick method on click and on keydown for
Enter/Space, and add aria-expanded bound to the open prop (and aria-controls if
applicable) so assistive tech can read the expanded state.
In `@packages/kit/src/storage/indexedDBStrategy.ts`:
- Around line 51-63: The upgrade handler currently only creates missing stores
and doesn't perform migrations, so existing 'conversations' entries using the
old full Conversation shape may become incompatible with the new
ConversationInfo type; update the upgrade(db, oldVersion, newVersion) logic to
be version-aware (use the passed oldVersion/newVersion), and when migrating from
versions that stored full Conversation objects, open the 'conversations'
objectStore, iterate all records, transform each Conversation into the new
ConversationInfo shape (selecting/renaming fields like id, title, updatedAt,
etc.), put the transformed records back into the 'conversations' store, and
likewise handle any schema changes for 'messages' if needed; ensure you only run
the migration steps when oldVersion is less than the target version to avoid
reprocessing.
In `@packages/kit/src/storage/utils.ts`:
- Around line 100-119: transformMessages can drop non-text renderContent because
when renderContent is an array but contains no 'collapsible-text' or
'markdown'/'text' items the function returns restMessage (which omits
renderContent) and loses data; update transformMessages to only return
restMessage when a transformation actually occurred (i.e.,
collapsibleTextItems.length > 0 or textItems.length > 0) and otherwise return
the original message object, keeping renderContent intact; modify the branch
that handles Array.isArray(renderContent) to detect if no items matched and
return message, otherwise set restMessage.reasoning_content/restMessage.content
as currently done and return restMessage.
- Around line 13-96: The unwrapProxy function currently uses a WeakSet and
returns empty placeholders for already-seen objects which loses shared data;
change the visited parameter to a WeakMap<object, any> that maps original
objects to their cloned result, so repeated encounters return the same clone
instead of an empty object/array. In unwrapProxy, after obtaining rawValue (via
toRaw), check visited.has(rawValue) and return visited.get(rawValue) if present;
before recursing into properties, create the clone placeholder (Array or
Object), store it in visited.set(rawValue, clone), then populate its entries
recursively (using unwrapProxy for prop values). Ensure arrays, Dates, RegExps,
ArrayBuffer/Blob still return directly and functions/getters/symbols are skipped
as before.
In `@packages/kit/src/vue/conversation/useConversation.ts`:
- Around line 116-123: The async loadConversations() call assigns directly to
conversations.value which can clobber any conversations created via
createConversation or switched via switchConversation before the promise
resolves; change the logic in the storage.loadConversations handling to merge
the loaded list into the existing conversations.value (e.g., upsert by unique
id) instead of replacing it, and ensure activeConversation is preserved or
remapped to the corresponding loaded item if needed; reference
storage.loadConversations, conversations.value, createConversation,
switchConversation, and activeConversation when implementing the merge/guard to
prevent overwriting in-memory state.
In `@packages/kit/src/vue/message/plugins/fallbackRolePlugin.ts`:
- Around line 9-17: The onBeforeRequest handler currently rebuilds
requestBody.messages from context.messages which overwrites prior plugin
mutations; change it to map over context.requestBody.messages
(requestBody.messages) instead so you preserve earlier edits, applying
fallbackRole only where message.role is falsy while keeping the rest of each
message's properties; update the mapping in onBeforeRequest to operate on
requestBody.messages and assign the result back to requestBody.messages.
In `@packages/kit/src/vue/message/useMessage.ts`:
- Around line 275-286: The loop incorrectly sets hasOnError = true for every
non-disabled plugin, causing errors to be swallowed even when no plugin
implements onError; update the loop in useMessage.ts so that hasOnError is set
to true only when plugin.onError exists (e.g., check plugin.onError before
setting the flag), then call plugin.onError with the context returned by
getBaseContext(ac.signal) and err; keep using isPluginDisabled(plugin, context)
to filter plugins and ensure the final if (!hasOnError) throw err remains
unchanged.
🟡 Minor comments (15)
packages/components/src/shared/composables/useAutoScroll.ts-134-137 (1)
134-137: Update the JSDoc return description to includearrivedState.The hook now returns
arrivedStatebut the doc comment still says it only returnsscrollToBottom. Please align the documentation with the new API.packages/components/src/bubble/composables/useMessageContent.ts-10-19 (1)
10-19: Add fallback handling for array indexing.The cast
as ChatMessageContentItemon line 13 masks the possibility thatArray.prototype.at()returnsundefined. While the current usage pattern guaranteescontentIndexis always valid (sourced from iterating the content array), this lacks defensive protection against future modifications or edge cases. Add an explicit fallback to align with defensive programming practices and match similar patterns elsewhere in the codebase.🛠️ Suggested fix
const content = computed(() => { const resolvedContent = contentResolver(props.message) - return Array.isArray(resolvedContent) - ? (resolvedContent.at(props.contentIndex) as ChatMessageContentItem) - : { type: 'text', text: resolvedContent || '' } + if (Array.isArray(resolvedContent)) { + const item = resolvedContent.at(props.contentIndex) as ChatMessageContentItem | undefined + return item ?? { type: 'text', text: '' } + } + return { type: 'text', text: resolvedContent || '' } })docs/demos/bubble/list-custom-group.vue-44-60 (1)
44-60: Use nullish timestamp checks to avoid mis-grouping ontimestamp: 0.Lines 55 and 57 use truthy checks on timestamps, which treats a valid
timestamp: 0as missing and collapses groups incorrectly. Replace with nullish checks (!= null) and cache the last message to eliminate repeated casting.🛠️ Suggested fix
+ const lastMsg = lastGroup?.messages[lastGroup.messages.length - 1] as + | (typeof messages)[0] + | undefined + const currTs = msgWithTimestamp.timestamp + const lastTs = lastMsg?.timestamp + - 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) - ) { + if (!lastGroup || (currTs != null && lastTs != null && currTs - lastTs > TIME_THRESHOLD)) {packages/components/src/bubble/composables/useToolCall.ts-69-84 (1)
69-84: Prevent stale asyncjsonrepairupdates.The
watchEffecttrackstoolCall.valueandtoolCallResults.valueas dependencies. When either changes while agetJsonrepair()promise is pending, a new promise launches. If the earlier promise resolves after the newer one, it overwritestoolCallWithResult.valuewith stale data. AddonInvalidateto cancel stale promise results.Suggested fix
- watchEffect(() => { + watchEffect((onInvalidate) => { const args = toolCall.value?.function.arguments const result = toolCallResults.value + let cancelled = false + onInvalidate(() => { + cancelled = true + }) + getJsonrepair() .then(({ jsonrepair }) => { + if (cancelled) return const repairedArgs = jsonrepair(typeof args === 'string' ? args || '{}' : JSON.stringify(args)) toolCallWithResult.value = { arguments: JSON.parse(repairedArgs), result: result ? JSON.parse(jsonrepair(result || '{}')) : undefined, } }) .catch((error) => { - console.warn(error) + if (!cancelled) console.warn(error) }) })packages/kit/src/vue/message/plugins/lengthPlugin.ts-9-19 (1)
9-19: Consider the execution order and potential infinite loop risk.Two observations:
Execution order:
requestNext()is called beforerestOptions.onAfterRequest, which may not allow downstream hooks to prevent or modify the continuation behavior.Infinite loop risk: If the model consistently returns
finish_reason: 'length', this plugin will repeatedly append messages and trigger new requests. Consider adding a safeguard (e.g., max continuation count).💡 Suggested safeguard
+const MAX_CONTINUATIONS = 5 + export const lengthPlugin = (options: UseMessagePlugin & { continueContent?: string } = {}): UseMessagePlugin => { - const { continueContent = 'Please continue with your previous answer.', ...restOptions } = options + const { continueContent = 'Please continue with your previous answer.', maxContinuations = MAX_CONTINUATIONS, ...restOptions } = options + let continuationCount = 0 return { name: 'length', ...restOptions, + onTurnStart: async (context) => { + continuationCount = 0 + return restOptions.onTurnStart?.(context) + }, onAfterRequest: async (context) => { const { lastChoice, appendMessage, requestNext } = context - if (lastChoice?.finish_reason === 'length') { + if (lastChoice?.finish_reason === 'length' && continuationCount < maxContinuations) { + continuationCount++ appendMessage({ role: 'user', content: continueContent }) requestNext() } return restOptions.onAfterRequest?.(context) }, } }docs/demos/bubble/list-auto-scroll.vue-11-14 (1)
11-14: Unusedrefattribute on container div.The
ref="containerRef"attribute is declared in the template butcontainerRefis never defined or used in the script. Either remove the ref or add the correspondingconst containerRef = ref()if it's needed for future functionality.Suggested fix (remove unused ref)
<div - ref="containerRef" style="height: 300px; border: 1px solid `#ddd`; border-radius: 4px; overflow-y: auto; padding: 8px" >packages/kit/src/utils.ts-170-172 (1)
170-172: Unhandled promise rejection fromreader.cancel().Unlike
handleSSEStream(Line 30) which catches errors fromreader.cancel(), this abort handler ignores the returned promise. If cancellation fails, the error will be silently swallowed.Suggested fix
// Set up abort signal listener const abortHandler = () => { - reader.cancel() + reader.cancel().catch((err) => console.error('Error cancelling reader:', err)) }packages/kit/src/vue/message/plugins/thinkingPlugin.ts-3-6 (1)
3-6: Plugin name can be unintentionally overwritten.Spreading
optionsafter definingnameallows callers to override the plugin's internal name, which could cause issues if other code depends on the plugin being named'thinking'.Proposed fix
export const thinkingPlugin = (options: UseMessagePlugin = {}): UseMessagePlugin => { + const { name: _, ...restOptions } = options return { name: 'thinking', - ...options, + ...restOptions, onCompletionChunk(context) {packages/kit/src/storage/localStorageStrategy.ts-78-85 (1)
78-85: Missing error handling in deleteConversation.Unlike other methods in this class,
deleteConversationlacks try-catch error handling. IflocalStorage.setItemthrows (e.g., quota exceeded), the error will propagate unexpectedly.🐛 Suggested fix
deleteConversation(conversationId: string) { + try { const conversations = getConversations(this.storageKey) const index = conversations.findIndex((item) => item.id === conversationId) if (index !== -1) { conversations.splice(index, 1) } localStorage.setItem(this.storageKey, JSON.stringify(conversations)) + } catch (error) { + console.error('删除会话失败:', error) + } }packages/kit/src/storage/localStorageStrategy.ts-53-64 (1)
53-64: Silent failure when saving messages for non-existent conversation.
saveMessagessilently does nothing if the conversation doesn't exist (whenindex === -1). This could hide bugs where messages are being saved to a deleted or non-existent conversation.Consider logging a warning or throwing an error when the conversation is not found.
🐛 Suggested improvement
saveMessages(conversationId: string, messages: ChatMessage[]) { try { const conversations = getConversations(this.storageKey) const index = conversations.findIndex((item) => item.id === conversationId) if (index !== -1) { conversations[index].messages = messages + localStorage.setItem(this.storageKey, JSON.stringify(conversations)) + } else { + console.warn(`Cannot save messages: conversation ${conversationId} not found`) } - localStorage.setItem(this.storageKey, JSON.stringify(conversations)) } catch (error) { console.error('保存会话消息失败:', error) } }packages/kit/src/vue/message/utils.ts-137-143 (1)
137-143: Truthy check may skip valid falsy values.The condition
if (targetValue)on line 137 will treat0,'', andfalseas non-existent, causing direct assignment instead of merge. If these are valid values that should trigger merge behavior, consider using explicitundefinedcheck.Suggested fix if falsy values should be preserved
- if (targetValue) { + if (targetValue !== undefined) {packages/kit/src/vue/message/plugins/toolPlugin.ts-264-326 (1)
264-326: Consider guardingrequestNext()when all tool calls are aborted.After
Promise.all(toolCallPromises)completes,requestNext()is called unconditionally on line 329. If the request was aborted and all tool calls returned early (line 314), this could trigger an unnecessary next request cycle.Suggested fix
await Promise.all(toolCallPromises) + + // Skip requestNext if aborted + if (abortSignal.aborted) { + return restOptions.onAfterRequest?.(context) + } + requestNext()packages/kit/src/storage/indexedDBStrategy.ts-149-162 (1)
149-162: Non-atomic deletion of conversation and messages.The delete operations for
conversationsandmessagesstores are executed sequentially without a transaction wrapper. If an error occurs between the two deletes, data could become inconsistent (orphaned messages).Suggested: Use single transaction
async deleteConversation(conversationId: string): Promise<void> { try { const db = await this.getDB() - - // 删除会话 - await db.delete('conversations', conversationId) - - // 删除该会话的消息记录(通过 conversationId 直接删除) - await db.delete('messages', conversationId) + const tx = db.transaction(['conversations', 'messages'], 'readwrite') + await Promise.all([ + tx.objectStore('conversations').delete(conversationId), + tx.objectStore('messages').delete(conversationId), + tx.done, + ]) } catch (error) {packages/kit/src/vue/conversation/useConversation.ts-164-181 (1)
164-181: Guard against duplicate conversation IDs
createConversationallows a caller-suppliedidbut doesn’t prevent collisions. If the id already exists, you’ll get duplicate entries and possibly overwriteworkingEngines, breakingfind/switch logic. Consider guarding or regenerating on collision.🛠️ Suggested guard
- const { id = generateId(), title, metadata, useMessageOptions } = params || {} + const { id: providedId = generateId(), title, metadata, useMessageOptions } = params || {} + let id = providedId + if (conversations.value.some((c) => c.id === id)) { + console.warn('[useConversation] duplicate conversation id, generating a new one:', id) + id = generateId() + }docs/src/components/bubble.md-255-256 (1)
255-256: Align contentIndex optionality with the type blockThe text says
contentIndexis optional, but the type snippet below shows it as required. Consider updating the wording to avoid confusion (e.g., “contentIndexis always provided; in single mode it’s 0”).🛠️ Suggested wording tweak
-Content 渲染器接收 `BubbleContentRendererProps` 作为 props,包含 `message` 和可选的 `contentIndex`。 +Content 渲染器接收 `BubbleContentRendererProps` 作为 props,包含 `message` 和 `contentIndex`(单内容模式下为 0)。
🧹 Nitpick comments (36)
docs/demos/examples/Assistant.vue (2)
447-454: AddContent-Typeheader and consider response status check.The fetch request is missing the
Content-Type: application/jsonheader, which may cause the server to misinterpret the request body. Additionally, there's no check forresponse.okbefore processing.♻️ Suggested improvement
responseProvider: async (requestBody, abortSignal) => { const response = await fetch('/api/chat/completions', { method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ ...requestBody, stream: true }), signal: abortSignal, }) + if (!response.ok) { + throw new Error(`Request failed: ${response.status}`) + } return sseStreamToGenerator(response, { signal: abortSignal }) },
465-467: Consider adding a fallback forisProcessing.For consistency with
messages(which has|| []), consider adding|| falsetoisProcessingto avoid passingundefinedto the:loadingprop.-const isProcessing = computed(() => messageEngine.value?.isProcessing.value) +const isProcessing = computed(() => messageEngine.value?.isProcessing.value ?? false)docs/demos/tools/utils/SSEStream.vue (1)
56-73: Clear the input after a successful submit.
Keeps the sender field in sync with typical chat UX.✨ UX tweak
messages.value.push({ role: 'user', content: content.trim(), }) + inputMessage.value = ''packages/kit/src/client.ts (1)
10-13: Consider adding migration guidance to the deprecation notice.The
@deprecatedannotation is appropriate, but it would be more helpful to include a reference to the replacement API or migration guide. This helps users understand what to use instead.💡 Suggested enhancement
/** - * `@deprecated` + * `@deprecated` Use `useConversation` and `useMessage` composables instead. + * See migration guide: docs/src/migration/use-conversation-migration.md * AI客户端类 */docs/demos/bubble/custom-renderer.vue (1)
36-53: Type safety could be improved in the custom renderer example.The props definition uses generic
Objecttype and the setup function relies on a double type assertion (as unknown as CodeMessage), which reduces type safety. Since this is a demo that users may copy, consider improving the typing:
- The
messageprop could useas PropType<BubbleMessage>for proper Vue type inference.- The double cast on line 49 suggests a type mismatch between what
useMessageContentreturns andCodeMessage.💡 Suggested improvement
+import type { PropType } from 'vue' +import type { BubbleMessage } from '@opentiny/tiny-robot' const CodeBlockRenderer = defineComponent({ props: { message: { - type: Object, + type: Object as PropType<BubbleMessage>, required: true, }, contentIndex: Number, },packages/components/src/bubble/renderers/ToolRole.vue (1)
10-20: Consider cleanup on unmount for tool call results.The
watchEffectwrites to shared store state but doesn't clean up when the component unmounts. If tool call results should be scoped to the component's lifecycle, consider usingonUnmountedto remove the entry. However, if the results are intentionally persisted across component lifecycles (e.g., for caching tool responses), the current approach is appropriate.💡 Optional cleanup pattern if results should be scoped
<script setup lang="ts"> -import { watchEffect } from 'vue' +import { watchEffect, onUnmounted } from 'vue' import { useBubbleStore } from '../composables' import { BubbleContentRendererProps } from '../index.type' const props = defineProps<BubbleContentRendererProps<string>>() const store = useBubbleStore<{ toolCallResults?: Record<string, string> }>() watchEffect(() => { if (!props.message.tool_call_id) { return } if (!store.toolCallResults) { store.toolCallResults = {} } store.toolCallResults[props.message.tool_call_id] = props.message.content ?? '' }) + +onUnmounted(() => { + if (props.message.tool_call_id && store.toolCallResults) { + delete store.toolCallResults[props.message.tool_call_id] + } +}) </script>docs/demos/bubble/state-change.vue (1)
43-46: Redundant state update intoggleLike.The
likedstate is already toggled on line 44, so callinghandleStateChangeon line 45 with the same value is redundant for local state management. If the intent is to demonstrate emitting state changes to a parent, consider adding a comment clarifying this pattern.Additionally, note that the checkbox on line 5 directly mutates
messageState.expandedviav-model, bypassinghandleStateChange, while the like button useshandleStateChange. This inconsistency may confuse users learning the state-change pattern.♻️ Suggested clarification
const toggleLike = () => { messageState.value.liked = !messageState.value.liked + // Emit state change event (useful when parent needs to sync state) handleStateChange({ key: 'liked', value: messageState.value.liked }) }docs/src/tools/storage.md (1)
190-221: Consider adding error handling note in custom strategy example.The
RemoteStorageStrategyexample omits error handling for brevity. Consider adding a brief comment or note that production implementations should handle network errors and non-2xx responses to avoid silent failures.📝 Optional: Add error handling note
async loadConversations(): Promise<ConversationInfo[]> { const response = await fetch(`${this.apiUrl}/conversations`) + // Production code should check response.ok and handle errors return response.json() }Or add a note after the example:
> **Note**: This example omits error handling for brevity. Production implementations should handle network failures and non-2xx responses.packages/components/src/bubble/renderers/Image.vue (1)
13-23: Consider adding defensive handling for unexpectedimage_urlformats.If
content.value.image_urlis neither a string nor an object with aurlproperty (e.g.,undefined,null, or malformed), Line 22 will throw. Consider adding a fallback:♻️ Suggested defensive handling
const imageUrl = computed(() => { if (!content.value) { return null } if (typeof content.value.image_url === 'string') { return content.value.image_url } - return content.value.image_url.url + return content.value.image_url?.url ?? null })docs/demos/bubble/provider-renderer.vue (1)
32-55: Consider aligning prop types with defineComponent's props definition.The
setupfunction explicitly typespropsasBubbleBoxRendererProps, but thepropsobject only definesplacementandshapeasString. This mismatch may cause TypeScript inconsistencies. For a demo, this is acceptable, but in production code consider defining props more precisely.packages/kit/src/vue/message/plugins/thinkingPlugin.ts (1)
19-24: Consider using.at(-1)for cleaner array access.Using
.at(-1)is more idiomatic for accessing the last element of an array.Suggested change
onTurnEnd(context) { // 如果不是流式数据或者请求被中断,thinking 状态可能不会被更新,在 onTurnEnd 中手动更新 - const lastMessage = context.currentTurn.slice(-1)[0] + const lastMessage = context.currentTurn.at(-1) if (lastMessage?.state) { lastMessage.state.thinking = undefined }docs/demos/bubble/tools.vue (1)
37-42: Consider wrappingJSON.parsein try-catch for robustness.While this is demo code,
JSON.parsecan throw ifargumentscontains malformed JSON, which could cause an unhandled exception.Suggested defensive approach
const handleChangeToolCallArguments = () => { const args = toolCalls.value[0]!.function.arguments - const parsedArgs = JSON.parse(args) - parsedArgs.a = parsedArgs.a + 1 - toolCalls.value[0]!.function.arguments = JSON.stringify(parsedArgs) + try { + const parsedArgs = JSON.parse(args) + parsedArgs.a = parsedArgs.a + 1 + toolCalls.value[0]!.function.arguments = JSON.stringify(parsedArgs) + } catch (e) { + console.warn('Failed to parse tool call arguments:', e) + } }packages/components/src/bubble/composables/useBubbleContentRenderer.ts (1)
49-53: Redundant nullish coalescing for required parameter.Since
contentIndexis now a requirednumberparameter (Line 40), the?? 0fallback on Line 51 is unnecessary and could be misleading.Suggested cleanup
const resolvedContent = contentResolver(msg) const content = Array.isArray(resolvedContent) - ? (resolvedContent.at(contentIndex ?? 0) as ChatMessageContentItem) + ? (resolvedContent.at(contentIndex) as ChatMessageContentItem) : { type: 'text', text: resolvedContent || '' }docs/demos/bubble/streaming.vue (1)
15-23: Prevent overlapping stream loops on repeated clicks.
If the button is clicked multiple times, concurrent loops can interleave. A simple token/cancel guard keeps output consistent.♻️ Proposed refactor
const fullText = '这是一段流式输出的文本内容。' const streamContent = ref('点击上方按钮开始流式输出文本') +let streamToken = 0 const resetStreamContent = async () => { + const token = ++streamToken streamContent.value = '' for (const char of fullText) { + if (token !== streamToken) break streamContent.value += char await new Promise((resolve) => setTimeout(resolve, 100)) } }packages/components/src/bubble/renderers/Tools.vue (1)
8-15: Consider normalizingtool_callsfor defensive consistency.While the Tools renderer is only instantiated when
tool_callsis guaranteed to be a non-empty array (via thefindcondition indefaultRenderers.ts), normalizing to an empty array aligns with the defensive patterns used elsewhere in the codebase and makes the component more resilient to future changes.♻️ Proposed refactor
const { restMessage, restProps } = useOmitMessageFields(props, ['tool_calls']) const renderer = useBubbleContentRenderer(restMessage, props.contentIndex) +const toolCalls = props.message.tool_calls ?? []- <Tool v-for="(tool, index) in props.message.tool_calls" :key="tool.id" v-bind="props" :tool-call-index="index" /> + <Tool v-for="(tool, index) in toolCalls" :key="tool.id" v-bind="props" :tool-call-index="index" />packages/components/src/bubble/composables/useBubbleBoxRenderer.ts (1)
50-55: Validation runs only once at setup time, not on reactive updates.The
contentIndexvalidation checkstoValue(messages).lengthduring function execution, but ifmessagesis reactive and changes later, this validation won't re-run. If the invariant must hold throughout the component's lifetime, consider moving this check inside thecomputedcallback or intogetContentAndIndex.♻️ Potential refactor to validate reactively
- // 如果 contentIndex 为数字,说明是 split 模式,当前 messages 数组长度为 1 - if (typeof contentIndex === 'number') { - if (toValue(messages).length !== 1) { - throw new Error('[BubbleBoxRenderer] When contentIndex is a number, messages array length must be 1') - } - } - const getContentAndIndex = (msgs: BubbleMessage[]) => { + // Validate split mode invariant + if (typeof contentIndex === 'number' && msgs.length !== 1) { + throw new Error('[BubbleBoxRenderer] When contentIndex is a number, messages array length must be 1') + } if (msgs.length !== 1) { return { content: undefined, index: undefined } }packages/components/src/bubble/renderers/Reasoning.vue (2)
36-50: Auto-scroll watch lacks cleanup and may trigger on unmounted component.The
watchonprops.message.reasoning_contentwithnextTickcould potentially execute after component unmount if the message updates rapidly. Consider adding a guard or usingwatchEffectwith cleanup.♻️ Safer approach with mounted guard
+import { nextTick, onBeforeUnmount, ref, watch, watchEffect } from 'vue' + +let isMounted = true +onBeforeUnmount(() => { + isMounted = false +}) watch( () => props.message.reasoning_content, () => { nextTick(() => { - if (!detailRef.value) { + if (!isMounted || !detailRef.value) { return }
56-59: Hardcoded Chinese strings may affect internationalization.The text "正在思考" and "已思考" are hardcoded. If the component library supports i18n, consider using translation keys or making these configurable via props/slots.
docs/demos/tools/storage/Custom.vue (1)
110-117: Consider adding error handling for fetch failures.The
responseProviderdoesn't handle network errors or non-2xx responses. While this is a demo, adding basic error handling would make it more robust and educational.♻️ Add basic error handling
responseProvider: async (requestBody, abortSignal) => { const response = await fetch(`${apiUrl}/api/chat/completions`, { method: 'POST', body: JSON.stringify({ ...requestBody, stream: true }), signal: abortSignal, }) + if (!response.ok) { + throw new Error(`Request failed: ${response.status}`) + } return sseStreamToGenerator(response, { signal: abortSignal }) },docs/demos/tools/storage/IndexedDB.vue (1)
60-67: Same error handling suggestion applies here.Similar to the Custom.vue demo, the
responseProviderlacks error handling for failed requests. Consider adding a guard for non-2xx responses for a more complete example.docs/demos/bubble/list.vue (1)
2-2: Consider renamingrolestoroleConfigsfor consistency.The variable is named
rolesbut is passed to the:role-configsprop. For consistency with the new API naming (as documented in the migration guide), consider renaming the variable toroleConfigs.♻️ Suggested change
- <tr-bubble-list :messages="messages" :role-configs="roles"></tr-bubble-list> + <tr-bubble-list :messages="messages" :role-configs="roleConfigs"></tr-bubble-list>-const roles: Record<string, BubbleRoleConfig> = { +const roleConfigs: Record<string, BubbleRoleConfig> = { ai: { placement: 'start', avatar: aiAvatar, }, user: { placement: 'end', avatar: userAvatar, }, }Also applies to: 20-29
packages/kit/src/types.ts (1)
1-1: Consider scoping the eslint-disable more narrowly.The file-wide
eslint-disablefor@typescript-eslint/no-explicit-anycould be scoped to specific lines whereanyis intentionally used (lines 28, 41-42) rather than disabling for the entire file.packages/components/src/bubble/renderers/Markdown.vue (1)
14-14: Type annotation could be clearer.The type
Awaited<ReturnType<typeof getMarkdownItAndDompurify>>with initial valuenullworks because the function likely returns a nullable type, but consider making this explicit for readability.♻️ Suggested improvement
-const markdownItAndDompurify = ref<Awaited<ReturnType<typeof getMarkdownItAndDompurify>>>(null) +const markdownItAndDompurify = ref<Awaited<ReturnType<typeof getMarkdownItAndDompurify>> | null>(null)docs/demos/tools/storage/LocalStorage.vue (1)
48-48: Inconsistent API URL construction compared to Basic.vue.This demo uses only
window.parent?.location.origin || location.origin, whileBasic.vueincludesbaseUrlfromimport.meta.env.BASE_URL. If the API is served from a subpath, requests may fail in this demo.♻️ Suggested fix for consistency
+// Get BASE_URL from import.meta if available +const baseUrl = (import.meta as any)?.env?.BASE_URL || '' -const apiUrl = window.parent?.location.origin || location.origin +const apiUrl = window.parent?.location.origin || location.origin + baseUrldocs/demos/tools/message/Basic.vue (1)
23-32: Consider simplifying the import.meta type augmentation.The custom interface declarations work but are verbose. Vue/Vite projects typically have these types available via
vite/clienttypes.♻️ Simplified alternative
-// Get BASE_URL from import.meta if available, otherwise use empty string -interface ImportMetaEnv { - BASE_URL?: string -} -interface ImportMetaWithEnv extends ImportMeta { - env?: ImportMetaEnv -} -const meta = typeof import.meta !== 'undefined' ? (import.meta as ImportMetaWithEnv) : null -const baseUrl = meta?.env?.BASE_URL || '' +const baseUrl = import.meta.env?.BASE_URL || ''If Vite types are not available, consider a simpler cast:
const baseUrl = (import.meta as any).env?.BASE_URL || ''docs/demos/bubble/reasoning.vue (1)
48-67: Consider adding cleanup for the animation on unmount.If the component unmounts during the replay animation, the async function will continue executing and attempt to update refs that may no longer exist.
♻️ Add cancellation support
+import { h, ref, onUnmounted } from 'vue' + +let animationAborted = false +onUnmounted(() => { + animationAborted = true +}) const replayThinking = async () => { if (reasoningState.value.thinking) { return } + animationAborted = false reasoningState.value.thinking = true reasoningContent.value = '' content.value = '' for (const char of rawReasoningContent) { + if (animationAborted) return await new Promise((resolve) => setTimeout(resolve, 10)) reasoningContent.value += char } reasoningState.value.thinking = false for (const char of rawContent) { + if (animationAborted) return await new Promise((resolve) => setTimeout(resolve, 10)) content.value += char } }docs/src/migration/use-message-migration.md (2)
7-7: Minor: Inconsistent version format throughout the document.The document mixes
v0.3.x(with 'v' prefix) and0.4.x(without prefix). Consider using a consistent format throughout for clarity.
125-128: Consider adding error handling note for JSON.parse in the example.The
JSON.parse(toolCall.function.arguments || '{}')could throw ifargumentscontains malformed JSON. While this is a simplified demo, a brief comment about production error handling would be helpful.📝 Suggested improvement
callTool: async (toolCall) => { - const args = JSON.parse(toolCall.function.arguments || '{}') + // In production, wrap in try-catch for malformed JSON + const args = JSON.parse(toolCall.function.arguments || '{}') return `Weather of ${args.city}: Sunny` },packages/components/src/bubble/Bubble.vue (1)
78-85: Conditional composable call may cause issues.Calling
useCopyCleanupconditionally at the component's top level breaks Vue's composition API rules. Althoughinjectreturns a static value here, ifBUBBLE_LIST_CONTEXT_KEYwere ever provided dynamically or if the component is reused in different contexts, this pattern could lead to inconsistent hook invocation.Consider always calling the composable but conditionally activating its behavior:
♻️ Suggested refactor
// 检查 Bubble 是否在 BubbleList 下 const isInBubbleList = inject(BUBBLE_LIST_CONTEXT_KEY, false) const bubbleRef = ref<HTMLDivElement | null>(null) -// 只有当 Bubble 不在 BubbleList 下时才使用 useCopyCleanup -if (!isInBubbleList) { - useCopyCleanup(bubbleRef) -} +// useCopyCleanup should handle the isInBubbleList check internally +useCopyCleanup(bubbleRef, { enabled: !isInBubbleList })Alternatively, if
useCopyCleanupcannot be modified, move the conditional logic inside the composable or ensure the composable gracefully handles being disabled.packages/kit/src/vue/message/useMessage.ts (1)
298-301: Minor: Redundant existence check.The check
if (currentTurn.slice(-1)[0])is unnecessary sincecurrentTurn.slice(-1)[0]?.loading = undefinedwould safely handle the undefined case with optional chaining.♻️ Suggested simplification
- if (currentTurn.slice(-1)[0]) { - currentTurn.slice(-1)[0].loading = undefined - } + const lastMessage = currentTurn.at(-1) + if (lastMessage) { + lastMessage.loading = undefined + }packages/kit/src/storage/localStorageStrategy.ts (1)
6-10: Consider defensive parsing for legacy data.The
getConversationshelper assumes stored data always has amessagesproperty. If there's legacy data without this field,conversation.messagescould beundefined, which may cause issues downstream.♻️ Suggested improvement
const getConversations = (storageKey: string) => { const conversationsStr = localStorage.getItem(storageKey) const conversations = conversationsStr ? JSON.parse(conversationsStr) : [] - return conversations as (ConversationInfo & { messages: ChatMessage[] })[] + return (conversations as (ConversationInfo & { messages?: ChatMessage[] })[]).map(c => ({ + ...c, + messages: c.messages ?? [] + })) }packages/kit/src/storage/types.ts (1)
1-2: MoveConversationInfoto shared types for proper layer separation.The
storage/layer importsConversationInfofrom../vue/conversation/types, creating an unwanted dependency from the storage layer to the Vue-specific layer.ConversationInfois a plain TypeScript interface (containing onlyid,title,createdAt,updatedAt, andmetadata) with no Vue-specific types, making it suitable for the shared types location.Move
ConversationInfotosrc/types.tsalongside other framework-agnostic types likeChatMessageandMaybePromiseto maintain proper architectural separation. This also affectslocalStorageStrategy.tsandindexedDBStrategy.ts, which currently depend on the same import.Note:
storage/utils.tsalso importstoRawfrom Vue; consider if this dependency should be revisited for consistency.packages/kit/src/vue/message/plugins/toolPlugin.ts (1)
293-300: Consider more specific error handling for JSON parse failures.When
JSON.parsefails on existingtoolMessage.content, the code logs a warning but continues with an empty object. This silently discards previously accumulated content. Consider preserving the original string content or logging a more descriptive message.Suggested improvement
let parsedContent: Record<string, any> = {} try { parsedContent = JSON.parse(toolMessage.content || '{}') } catch (error) { - console.warn(error) + console.warn('Failed to parse tool message content as JSON, starting fresh:', error) }packages/kit/src/vue/message/utils.ts (1)
165-168: Potential performance concern with large index Maps.Using
Math.max(...Array.from(targetMap.keys()))with spread operator could cause stack overflow for very large arrays (browser-dependent, typically ~100k+ elements). For typical chat scenarios this is likely fine, but consider a safer approach for robustness.Safer max calculation
- const arrLen = Math.max(...Array.from(targetMap.keys()), -1) + 1 + const arrLen = Array.from(targetMap.keys()).reduce((max, k) => Math.max(max, k), -1) + 1docs/demos/tools/conversation/IndexedDB.vue (1)
92-102: Consider extracting database name to a constant.The database name
'demo-chat-db'is duplicated on lines 70 and 96. Extracting to a constant would prevent sync issues.Suggested refactor
+const DB_NAME = 'demo-chat-db' + const { // ... } = useConversation({ // ... storage: indexedDBStorageStrategyFactory({ - dbName: 'demo-chat-db', + dbName: DB_NAME, dbVersion: 1, }), }) const clearStorage = async () => { if (confirm('确定要清空所有会话数据吗?')) { try { - indexedDB.deleteDatabase('demo-chat-db') + indexedDB.deleteDatabase(DB_NAME) location.reload()packages/kit/src/storage/indexedDBStrategy.ts (1)
77-81: Consider cursor-based iteration for large datasets.Using
getAllFromIndexfollowed byreverse()loads all conversations into memory. For users with many conversations, consider using a cursor with'prev'direction for direct reverse iteration.Suggested optimization for large datasets
async loadConversations(): Promise<ConversationInfo[]> { try { const db = await this.getDB() - - // 按更新时间倒序获取所有会话 - const conversations = await db.getAllFromIndex('conversations', 'by-updated') - - // 最新的在前 - return conversations.reverse() + const conversations: ConversationInfo[] = [] + const tx = db.transaction('conversations', 'readonly') + const index = tx.store.index('by-updated') + + // Iterate in reverse (newest first) using 'prev' direction + let cursor = await index.openCursor(null, 'prev') + while (cursor) { + conversations.push(cursor.value) + cursor = await cursor.continue() + } + + return conversations } catch (error) {
- Simplified the content extraction in `useBubbleBoxRenderer` by removing unnecessary type imports and ensuring consistent content resolution. - Enhanced `useCopyCleanup` to utilize `onCleanup` for better event listener management, improving performance and preventing memory leaks. - Updated `Markdown.vue` to sanitize rendered content after processing, ensuring security against XSS attacks. - Added HTML escaping in `Tool.vue` to prevent XSS vulnerabilities when rendering JSON content. - Refined `unwrapProxy` function in `utils.ts` to use a `WeakMap` for better handling of circular references and shared objects. - Improved error handling in `useMessage` by ensuring `onError` is only called if defined, enhancing plugin robustness. - Adjusted `fallbackRolePlugin` to correctly map messages, ensuring fallback roles are applied consistently.
…rategy - Improved the loading mechanism in `useConversation` to handle empty conversation lists gracefully. - Implemented a merging strategy that prioritizes in-memory conversations over stored ones, ensuring that newly created conversations are not overwritten. - Added checks to maintain the integrity of the active conversation after merging, preventing accidental loss of context.
…e demo examples - Removed the '存储策略' sidebar item from the theme configuration to simplify navigation. - Added mock response and storage strategies to the conversation demo, allowing for a complete offline experience. - Updated the Basic.vue demo to include a message sender and delete conversation functionality, improving user interaction. - Introduced new utility files for mock response and storage strategies, enhancing the flexibility of the conversation management system. - Enhanced documentation for conversation management, providing clearer examples and usage instructions.
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: 10
🤖 Fix all issues with AI agents
In `@docs/demos/tools/message/Basic.ts`:
- Around line 19-25: In responseProvider (the async function that calls fetch
and returns sseStreamToGenerator), add the Content-Type: application/json header
to the fetch request and immediately check response.ok after the fetch; if not
ok, read response.text() (or json()) and throw or return a rejected Error with
that body so you don't call sseStreamToGenerator on an error response; update
references to apiUrl/responseProvider and keep sseStreamToGenerator usage only
for successful responses.
In `@docs/demos/tools/message/CustomChunk.ts`:
- Around line 20-27: In the responseProvider passed to useMessage, add a
"Content-Type: application/json" header to the fetch and check the HTTP status
before streaming: after await fetch(`${apiUrl}/api/chat/completions`, ...)
inspect response.ok (or response.status) and throw or handle a non-2xx response
(including reading response.text() for an error message) instead of calling
sseStreamToGenerator on an error response; ensure you still pass abortSignal and
the stream to sseStreamToGenerator when response.ok is true.
In `@docs/demos/tools/message/ErrorHandling.ts`:
- Around line 11-13: The apiUrl construction uses wrong operator precedence so
baseUrl is only appended when window.parent is missing; update the expression
for apiUrl (the apiUrl variable) to parenthesize the origin selection: compute
origin as (window.parent?.location.origin || location.origin) and then append
baseUrl, i.e. ensure you use parentheses so baseUrl is always concatenated to
the chosen origin; check the variables meta and baseUrl remain unchanged.
In `@docs/demos/tools/message/MockStream.vue`:
- Around line 30-33: The handleSubmit function currently calls sendMessage with
whatever content is provided; add input validation to prevent sending empty or
whitespace-only messages and avoid sending while processing: in handleSubmit
(and related UI binding using inputMessage.value) check if (!content?.trim() ||
isProcessing.value) return before calling sendMessage, and only clear
inputMessage.value after a successful send.
In `@docs/demos/tools/message/NonStreaming.ts`:
- Around line 19-26: In responseProvider (the async function assigned to
responseProvider), add the Content-Type: application/json header to the fetch
request and validate the HTTP response before calling response.json();
specifically, after await fetch(...) check response.ok and if false throw or
return a rejected Error including response.status and await response.text() for
diagnostics, otherwise return response.json(); keep stream: false in the JSON
body and preserve the abortSignal.
In `@docs/demos/tools/message/OnBeforeRequest.ts`:
- Around line 31-37: The responseProvider implementation is missing the
Content-Type header and lacks response status validation; update the fetch call
in responseProvider to include headers: { 'Content-Type': 'application/json' }
and after awaiting fetch check response.ok (e.g., throw an Error including
response.status and await response.text() for details) before calling
sseStreamToGenerator so sseStreamToGenerator only receives a successful
response; reference the responseProvider function and sseStreamToGenerator and
keep using abortSignal for the fetch signal.
In `@docs/demos/tools/message/RequestState.ts`:
- Around line 18-26: responseProvider currently uses a fixed 1.5s delay that
ignores abortSignal and proceeds to stream even for non-OK HTTP responses;
change the artificial delay to be abort-aware (reject immediately when
abortSignal.aborted or hook signal into a cancellable Promise) so the function
aborts without waiting, and after fetch check response.ok and throw an Error
(include status/statusText) before calling sseStreamToGenerator; reference
responseProvider, abortSignal, fetch, response.ok, and sseStreamToGenerator when
making these changes.
- Around line 9-11: The top-level apiUrl computation uses
window.parent.location.origin which can throw in SSR or cross-origin iframes;
change it to compute apiUrl lazily and guard access by checking typeof window
!== 'undefined' before touching window, wrap any access to
window.parent.location.origin in a try/catch or conditionally compare
window.parent === window to avoid cross-origin reads, and fall back to
location.origin + baseUrl or an empty string; update the symbols meta, baseUrl
and apiUrl in RequestState.ts so meta/baseUrl remain top-level but apiUrl is
derived inside a safe function or getter that performs these runtime guards.
In `@docs/demos/tools/message/RequestState.vue`:
- Around line 39-41: The handleSubmit function currently calls
sendMessage(content) without awaiting it and clears inputMessage.value
immediately; change it to await sendMessage(content) and only set
inputMessage.value = '' after the awaited call succeeds (wrap in try/catch
around sendMessage to handle errors and avoid unhandled promise rejections,
e.g., call await sendMessage(content) inside try and clear inputMessage.value
there, and handle or rethrow the error in catch). Ensure you modify the
handleSubmit function reference so the promise is awaited and errors are caught.
In `@docs/src/tools/message.md`:
- Line 164: Clarify plugin activation in the `useMessage` docs: state that
`thinkingPlugin` is automatically active by default (no need to add to
`plugins`), while `fallbackRolePlugin`, `lengthPlugin`, and `toolPlugin` are
built-in but opt-in and only take effect if explicitly included in the `plugins`
array; also note that supplying `thinkingPlugin(...)` in `plugins` will
override/customize the default behavior if users want to change or disable it.
🧹 Nitpick comments (9)
docs/src/tools/message.md (3)
23-23: Document or link tosseStreamToGeneratorutility.The text mentions
sseStreamToGeneratoras a utility function to convert SSE responses to async generators, but doesn't show where to import it from or link to its documentation. Users encountering this for the first time won't know how to use it.📝 Suggested addition to clarify usage
Consider adding a brief note after line 23:
使用 `responseProvider` 发起流式请求,配合 `initialMessages` 展示欢迎语。当后端返回 SSE(Server-Sent Events)流时,可使用 `sseStreamToGenerator` 工具函数(从 `@opentiny/tiny-robot-kit` 导入)将 `fetch` 的 `Response` 转为异步生成器(`AsyncGenerator`),供 `useMessage` 逐块消费并合并到消息内容中。 或者在 API 部分添加一个"工具函数"小节专门说明 sseStreamToGenerator 的签名和用法。
83-92: Clarify the interaction betweenrequestMessageFieldsandrequestMessageFieldsExclude.The current explanation describes the filtering logic twice (lines 85-86 and 90-91) with similar wording, which may confuse readers. The interaction between whitelist and blacklist could be explained more clearly and concisely.
📝 Proposed clearer explanation
Consider consolidating and clarifying the logic:
/** * 请求消息时,要包含的字段(白名单)。默认包含所有字段。 - * 如果 `requestMessageFieldsExclude` 存在,会先取 `requestMessageFields` 中的字段,再排除 `requestMessageFieldsExclude` 中的字段 + * 若同时指定黑名单,则先应用白名单筛选,再从结果中排除黑名单字段。 */ requestMessageFields?: string[] /** - * 请求消息时,要排除的字段(黑名单)。默认会排除 `state`、`metadata`、`loading` 字段(这几个字段是给UI展示用的)。 - * 如果 `requestMessageFields` 存在,会先取 `requestMessageFields` 中的字段,再排除 `requestMessageFieldsExclude` 中的字段 + * 请求消息时,要排除的字段(黑名单)。 + * 默认排除: `state`、`metadata`、`loading`(仅用于 UI 展示)。 + * 若同时指定白名单,则先应用白名单筛选,再从结果中排除黑名单字段。 */ requestMessageFieldsExclude?: string[]Or add a note after both fields explaining the precedence:
> **字段筛选逻辑**: 若同时指定 `requestMessageFields`(白名单)和 `requestMessageFieldsExclude`(黑名单),则先应用白名单筛选出指定字段,再从结果中排除黑名单字段。若仅指定白名单,则只保留白名单字段;若仅指定黑名单,则从所有字段中排除黑名单字段。
119-120: Consider reformatting for improved readability.The explanation of
responseProviderreturn values is comprehensive but presented as a single dense paragraph. Breaking it into structured points would make it easier to understand.📖 Proposed structured format
**responseProvider 返回值**: `responseProvider` 的返回值决定响应模式: - **非流式**(`Promise<T>`): 一次性得到完整结果,适用于不支持 SSE 的后端(`stream: false`)。`useMessage` 会将解析出的内容整体写入消息。 - **流式**(`AsyncGenerator<T>` 或 `Promise<AsyncGenerator<T>>`): 逐块产出数据,适用于流式接口(如 SSE)。`useMessage` 会按块消费并增量合并到消息内容中。 - **SSE 转换**: 若后端返回 SSE 流,可使用 `sseStreamToGenerator` 将 `fetch` 的 `Response` 转为异步生成器。docs/demos/tools/message/CustomChunk.ts (1)
4-12: Consider extracting the sharedapiUrlcomputation to a utility.This
ImportMetaEnv/ImportMetaWithEnvinterface pattern andapiUrlcomputation is duplicated across multiple demo files (Basic.ts,OnBeforeRequest.ts,NonStreaming.ts). Extracting this to a shared utility (e.g.,docs/demos/tools/utils/apiUrl.ts) would reduce duplication and simplify future maintenance.docs/demos/tools/message/ToolCall.ts (1)
96-98: Consider wrappingJSON.parsein try-catch for robustness.If
toolCall.function?.argumentscontains malformed JSON,JSON.parsewill throw an unhandled exception. While this is demo code, adding error handling improves reliability and serves as a better example.🛡️ Suggested defensive parsing
callTool: async (toolCall) => { - const args = JSON.parse(toolCall.function?.arguments || '{}') + let args: Record<string, any> = {} + try { + args = JSON.parse(toolCall.function?.arguments || '{}') + } catch { + return '工具调用参数解析失败。' + } return `${args.city} 天气:晴,25°C。` },docs/demos/tools/conversation/mockStorageStrategy.ts (1)
61-63: Minor: Redundant fallback inloadConversations.The
|| []fallback is unnecessary sincethis.conversationsis always initialized as an array at declaration time.♻️ Suggested simplification
async loadConversations(): Promise<ConversationInfo[]> { - return this.conversations || [] + return this.conversations }docs/demos/tools/message/MockStream.ts (1)
5-28: Consider extracting shared mock stream logic.This
mockStreamimplementation is nearly identical tomockResponseProviderindocs/demos/tools/conversation/mockResponseProvider.ts, differing only in delay (30ms vs 150ms) and reply text. For demo files, self-contained examples are acceptable, but if more mock providers are added, consider extracting a shared utility with configurable delay and message.docs/demos/tools/message/ErrorHandling.ts (1)
18-27: Consider defensive check for emptycurrentTurn.The non-null assertion
currentTurn.at(-1)!assumescurrentTurnalways has at least one message. While this is typically true in the normal message flow, adding a guard would prevent potential runtime errors in edge cases.Proposed defensive check
onError({ currentTurn, error }) { const message = error instanceof Error ? error.message : String(error) const lastMessage = currentTurn.at(-1)! + if (!lastMessage) return if (error instanceof Error && error.name === 'ErrorRenderer') {docs/src/tools/conversation.md (1)
108-109: ClarifysaveMessagesbehavior whenidis omitted.The comment says "保存指定会话的消息" (save messages of specified conversation), but
idis optional. Consider documenting that whenidis omitted, it saves the active conversation's messages (if that's the intended behavior).Suggested documentation update
- /** 保存指定会话的消息 */ + /** 保存指定会话的消息,不传 id 时保存当前活跃会话 */ saveMessages: (id?: string) => void
- Added 'Content-Type' header to fetch requests in various demo components to ensure proper JSON handling. - Implemented error handling for API responses, throwing an error with status information for better debugging. - Updated multiple demo files including Assistant.vue, IndexedDB.vue, LocalStorage.vue, and others to improve robustness in message processing.
- Clarified the default activation status of built-in plugins: `fallbackRolePlugin`, `thinkingPlugin`, and `lengthPlugin`. - Specified that `toolPlugin` is an optional plugin that requires explicit addition to the `plugins` array. - Updated descriptions for each plugin to include information on customization and default behavior, improving clarity for users.
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: 3
🤖 Fix all issues with AI agents
In `@docs/demos/tools/conversation/IndexedDB.vue`:
- Line 49: The current apiUrl assignment reads window.parent.location.origin
which can throw a SecurityError in cross-origin embeds; change the logic around
the apiUrl variable to safely detect/access parent origin: either first check if
window.parent === window (same window) or wrap access to
window.parent.location.origin in a try/catch and fall back to location.origin on
any exception; update the apiUrl initialization (the const named apiUrl) so it
uses the guarded check/try-catch and always resolves to a safe origin string.
In `@docs/demos/tools/message/OnBeforeRequest.ts`:
- Around line 10-12: The code reads window.parent.location.origin directly when
computing apiUrl (using meta and baseUrl) which can throw a SecurityError for
cross‑origin embeds; change the apiUrl assignment to attempt reading
window.parent.location.origin inside a try/catch and on any error or if
undefined fall back to location.origin + baseUrl so the hook never throws—keep
the existing meta and baseUrl variables and only wrap the parent origin access
for apiUrl.
In `@docs/demos/tools/storage/Custom.vue`:
- Line 95: The assignment to the apiUrl constant can throw a DOMException when
accessing window.parent.location.origin in cross-origin iframes; update the
apiUrl initialization (where apiUrl is defined in Custom.vue) to safely resolve
origin by wrapping the window.parent access in a try/catch (or checking
window.parent === window) and falling back to location.origin, and ensure any
caught DOMException is ignored or logged so the component can still mount.
🧹 Nitpick comments (2)
docs/demos/tools/conversation/LocalStorage.vue (1)
74-76: Avoid duplicating the storage key string.
Centralize it to prevent mismatches if it changes.♻️ Proposed refactor
-// 使用 LocalStorage 策略 +const STORAGE_KEY = 'demo-conversations-localstorage' + +// 使用 LocalStorage 策略 const { activeConversation, activeConversationId, conversations, createConversation, switchConversation, abortActiveRequest, } = useConversation({ useMessageOptions: { @@ }, storage: localStorageStrategyFactory({ - key: 'demo-conversations-localstorage', // 自定义存储键名 + key: STORAGE_KEY, // 自定义存储键名 }), }) @@ const clearStorage = () => { if (confirm('确定要清空所有会话数据吗?')) { - localStorage.removeItem('demo-conversations-localstorage') + localStorage.removeItem(STORAGE_KEY) location.reload() } }Also applies to: 96-99
docs/demos/tools/storage/Custom.vue (1)
46-79: Consider cloning message objects to avoid shared-reference mutations.
saveMessages/loadMessagesreturn shallow array copies, but the message objects are still shared. In-memory storage can then “persist” in-place mutations without explicit saves, which differs from persistent strategies. A small clone helper keeps behavior consistent.♻️ Suggested change (clone on save/load)
class MemoryStorageStrategy implements ConversationStorageStrategy { private conversations: ConversationInfo[] = [] private messagesMap: Map<string, ChatMessage[]> = new Map() + private cloneMessages(messages: ChatMessage[]): ChatMessage[] { + return JSON.parse(JSON.stringify(messages)) + } loadMessages(conversationId: string): ChatMessage[] { - return [...(this.messagesMap.get(conversationId) || [])] + return this.cloneMessages(this.messagesMap.get(conversationId) || []) } saveMessages(conversationId: string, messages: ChatMessage[]): void { - this.messagesMap.set(conversationId, [...messages]) + this.messagesMap.set(conversationId, this.cloneMessages(messages)) } }
🧹 Preview Cleaned UpThe preview deployment has been removed. |

PR 描述:v0.4.0 重大组件重构与增强
📊 概览
本次 PR 对 Bubble 组件系统和核心消息/会话钩子进行了重大增强,代表从 v0.3.x 到 v0.4.x 的重大升级。变更包括架构改进、新功能和全面的迁移指南。
🎯 主要变更
1. Bubble 组件 v0.4 重大升级
核心架构变更
BubbleList从itemsprop 改为messagesprop,支持消息分组、状态、推理和工具调用priority+find()模式新功能
consecutive、divider、自定义函数)stateprop 和state-change事件,用于 UI 状态管理而不污染原始消息contentResolver函数用于灵活的内容字段提取contentRenderMode(single|split) 用于数组内容渲染Image- 图片内容渲染Markdown- 增强的 markdown 渲染Loading- 加载状态显示Reasoning- 推理内容显示,带增强样式和动画Tool/Tools/ToolRole- 工具调用渲染API 变更
BubbleList:items→messages,roles→roleConfigsgroupStrategy、dividerRole、fallbackRole、contentResolver、contentRenderModeBubble:新增shape: 'none'选项,增强avatar支持 Component 类型2. useMessage Hook 重构
架构改进
fallbackRolePlugin、thinkingPlugin、lengthPlugin、toolPluginresponseProvider函数替代直接使用clientPromise<T>(非流式)和AsyncGenerator<T>(流式)requestState:'idle' | 'processing' | 'completed' | 'error'processingState:详细的处理信息requestMessageFields:指定请求中包含哪些字段requestMessageFieldsExclude:指定请求中排除哪些字段(默认:['state', 'metadata', 'loading'])3. useConversation Hook 增强
主要改进
useMessage引擎实例loadConversations、loadMessages、saveConversation、saveMessagesIndexedDBStrategy和LocalStorageStrategy实现ConversationStorageStrategy接口支持自定义存储策略autoSaveMessages和autoSaveThrottle选项useThrottleFn进行高效节流(leading + trailing)新工具函数
useThrottleFn:带 leading/trailing 选项的节流函数实现createStorageStrategy、validateStorageStrategy等4. 存储系统增强
新存储工具(
packages/kit/src/storage/utils.ts)增强的存储策略
5. 新组件和组合式函数
Bubble 组合式函数
useContentResolver:从消息中解析内容useCopyCleanup:清理复制的文本useToolCall:增强的工具调用管理useBubbleBoxRenderer:Box 渲染器解析useBubbleContentRenderer:Content 渲染器解析新渲染器组件
Image.vue:图片内容渲染器Markdown.vue:增强的 markdown 渲染器Loading.vue:加载状态渲染器Reasoning.vue:带动画的推理内容渲染器Tool.vue、Tools.vue、ToolRole.vue6. 文档和迁移指南
新迁移指南
docs/src/migration/bubble-migration.md):从 v0.3.x 迁移到 v0.4.x 的全面指南docs/src/migration/use-message-migration.md):迁移 useMessage hook 的指南docs/src/migration/use-conversation-migration.md):迁移 useConversation hook 的指南更新的文档
docs/src/tools/storage.md)docs/src/tools/utils.md)新示例
7. Bug 修复和改进
contentIndexuseBubbleBoxRenderer中的内容提取逻辑🔄 破坏性变更
Bubble 组件
BubbleList.items→BubbleList.messages(必需变更)BubbleList.roles→BubbleList.roleConfigs(命名变更)useMessage Hook
clientprop,替换为responseProvideruseConversation Hook
clientprop,替换为useMessageOptions🧪 测试
📦 文件变更摘要
核心组件
packages/components/src/bubble/*- 重大重构packages/kit/src/vue/message/*- 插件系统和重构packages/kit/src/vue/conversation/*- 增强懒加载和存储策略packages/kit/src/storage/*- 新工具和增强策略🎉 优势
Summary by CodeRabbit
New Features
Documentation
Enhancements
✏️ Tip: You can customize this high-level summary in your review settings.