
Use X Chat
- 66 installs
- 4.7k repo stars
- Updated August 4, 2026
- ant-design/x
use-x-chat is a skill that explains the @ant-design/x-sdk useXChat hook for building AI conversation apps with provider integration, message management, and multi-conversation support.
About
This skill explains how to use the @ant-design/x-sdk useXChat hook to build AI conversation applications. A developer uses it for custom Provider integration, message state management, error and abort handling, and multi-conversation management. It shows how to map the hook's MessageInfo array into Bubble.List items and wire the Sender component for sending and cancelling requests.
- Explains the useXChat hook for building AI conversation apps
- Covers custom Provider integration, message management, and multi-conversation handling
- Shows mapping MessageInfo to Bubble.List items and error/abort fallbacks
Use X Chat by the numbers
- 66 all-time installs (skills.sh)
- Ranked #6,006 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
use-x-chat capabilities & compatibility
- Capabilities
- ant design x · x card
- Use cases
- frontend · ui design · orchestration
What use-x-chat says it does
Use the `useXChat` Hook to build professional AI conversation applications.
`messages` is `MessageInfo<ChatMessage>[]` and cannot be passed directly to `Bubble.List`. It must be mapped to `{ key, role, content, loading }` format.
npx skills add https://github.com/ant-design/x --skill use-x-chatAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 66 |
|---|---|
| repo stars | ★ 4.7k |
| Last updated | August 4, 2026 |
| Repository | ant-design/x ↗ |
What it does
Build AI chat apps with the useXChat hook: provider integration, message state, and Bubble.List rendering.
Who is it for?
Developers building AI conversation UIs with @ant-design/x-sdk once a custom Chat Provider exists.
Skip if: Building the Chat Provider itself (use the x-chat-provider skill) or non-x chat stacks.
When should I use this skill?
The user is integrating useXChat, managing chat messages, handling errors, or building multi-conversation UIs with @ant-design/x-sdk.
What you get
A working useXChat integration with mapped Bubble.List rendering, placeholders, and error/abort fallbacks.
- useXChat integration code
- Bubble.List message mapping
By the numbers
- requires @ant-design/x-sdk 2.2.2+
- three-step integration (provider, usage, UI)
Files
🎯 Skill Positioning
Core Positioning: Use the useXChat Hook to build professional AI conversation applications. Prerequisite: Already have a custom Chat Provider (refer to x-chat-provider skill)Table of Contents
- 🚀 Quick Start
- 🧩 Core Concepts
- Data Model
- Configuration Options
- Return Values
- 🔧 Core Function Details
- 🗂️ Multi-conversation Management
- 📋 Prerequisites and Dependencies
- 🚨 Development Rules
- 🔗 Reference Resources
🚀 Quick Start
1. Dependency Management
- @ant-design/x-sdk: 2.2.2+
- @ant-design/x: latest version (UI components)
npm install @ant-design/x-sdk@latest @ant-design/x@latest2. Three-step Integration
Step 1: Prepare Provider
Handled by the x-chat-provider skill. Note XRequest must pass manual: true:
import { MyChatProvider } from './MyChatProvider';
import { XRequest } from '@ant-design/x-sdk';
// ⚠️ manual: true is required
const provider = new MyChatProvider({
request: XRequest('https://your-api.com/chat', { manual: true }),
});Step 2: Basic Usage
import { useXChat } from '@ant-design/x-sdk';
const ChatComponent = () => {
const { messages, onRequest, isRequesting } = useXChat({
provider,
requestPlaceholder: (_, { messages }) => ({
content: 'Thinking...',
role: 'assistant',
}),
requestFallback: (_, { error, messageInfo }) => {
if (error.name === 'AbortError') {
return { content: messageInfo?.message?.content || 'Reply cancelled', role: 'assistant' };
}
return { content: 'Network error, please try again later', role: 'assistant' };
},
});
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
{msg.message.role}: {msg.message.content}
</div>
))}
<button onClick={() => onRequest({ query: 'Hello' })}>Send</button>
</div>
);
};Step 3: UI Integration
⚠️messagesisMessageInfo<ChatMessage>[]and cannot be passed directly toBubble.List. It must be mapped to{ key, role, content, loading }format. `Bubble.List` uses the `role` prop (notroles) to configure role styles.
import { Bubble, Sender } from '@ant-design/x';
const ChatUI = () => {
const { messages, onRequest, isRequesting, abort } = useXChat({ provider });
return (
<div style={{ height: 600 }}>
<Bubble.List
// ✅ Correct: use role (not roles)
role={{
user: { placement: 'end' },
assistant: { placement: 'start' },
}}
items={messages.map(({ id, message, status }) => ({
key: id,
role: message.role, // matches role config key
content: message.content, // message content
loading: status === 'loading', // loading animation
}))}
/>
<Sender
loading={isRequesting}
onSubmit={(content) => onRequest({ query: content })}
onCancel={abort}
/>
</div>
);
};When ChatMessage is an object type (not string)
When ChatMessage is a complex object (e.g., with content, attachments fields), use contentRender:
<Bubble.List
role={{
assistant: {
placement: 'start',
// contentRender receives content param, which is the message field itself
contentRender(content: MyMessage) {
return (
<div>
<div>{content.content}</div>
{content.attachments?.map((a) => (
<FileCard key={a.url} name={a.name} />
))}
</div>
);
},
},
user: {
placement: 'end',
contentRender(content: MyMessage) {
return content.content;
},
},
}}
items={messages.map(({ id, message, status }) => ({
key: id,
role: message.role,
content: message, // ⚠️ Pass the entire message object; contentRender handles rendering
loading: status === 'loading',
}))}
/>🧩 Core Concepts
Data Model
⚠️ Important:messagestype isMessageInfo<ChatMessage>[]; message content is inmsg.message
interface MessageInfo<ChatMessage> {
id: number | string; // Message unique identifier
message: ChatMessage; // Actual message content (your ChatMessage type)
status: MessageStatus; // Message status
extraInfo?: AnyObject; // Extended info (note: extraInfo, not extra)
}
type MessageStatus = 'local' | 'loading' | 'updating' | 'success' | 'error' | 'abort';
// local: locally sent user message
// loading: AI reply placeholder (corresponds to requestPlaceholder)
// updating: AI streaming output in progress
// success: AI reply complete
// error: request failed
// abort: user actively cancelleduseXChat Configuration Options
| Option | Type | Description |
|---|---|---|
provider | AbstractChatProvider<ChatMessage, Input, Output> | Required, Provider instance |
conversationKey | string | Conversation unique identifier, required for multi-conversation |
defaultMessages | `DefaultMessageInfo[] \ | () => ... \ |
requestPlaceholder | `ChatMessage \ | (requestParams, { messages }) => ChatMessage` |
requestFallback | `ChatMessage \ | (requestParams, { error, errorInfo, messages, messageInfo }) => ChatMessage \ |
parser | `(message: ChatMessage) => BubbleMessage \ | BubbleMessage[]` |
requestFallback'smessageInfotype isMessageInfo<ChatMessage>, the message being updated when the request fails.requestFallbackhandles both network errors (error) and user abort (error.name === 'AbortError').
useXChat Return Values
| Return Value | Type | Description |
|---|---|---|
messages | MessageInfo<ChatMessage>[] | Message list; must be mapped before passing to Bubble.List |
parsedMessages | MessageInfo<ParsedMessage>[] | Message list after parser transform (use this when parser is set) |
onRequest | (params: Partial<Input>, opts?: { extraInfo: AnyObject }) => void | Add message and trigger request |
isRequesting | boolean | Whether request is in progress |
abort | () => void | Abort current request |
setMessages | (messages: Partial<MessageInfo<ChatMessage>>[]) => void | Directly modify message list, no request triggered |
setMessage | `(id: string \ | number, info: Partial<MessageInfo<ChatMessage>>) => void` |
removeMessage | `(id: string \ | number) => boolean` |
onReload | `(id: string \ | number, params: Partial<Input>, opts?: { extraInfo: AnyObject }) => void` |
queueRequest | `(conversationKey: string \ | symbol, params: Partial<Input>, opts?: { extraInfo: AnyObject }) => void` |
isDefaultMessagesRequesting | boolean | Whether default messages are async loading |
🔧 Core Function Details
Core functionality reference: CORE.md
🗂️ Multi-conversation Management
useXConversations Hook
useXConversations is a conversation list management Hook provided by @ant-design/x-sdk, used together with useXChat for multi-conversation:
import { useXConversations } from '@ant-design/x-sdk';
import type { ConversationData } from '@ant-design/x-sdk';
const {
conversations, // ConversationData[]: conversation list
activeConversationKey, // string: currently active conversation key
setActiveConversationKey, // (key: string) => void: switch conversation
addConversation, // (ConversationData, placement?) => boolean
removeConversation, // (key: string) => boolean
setConversation, // (key: string, ConversationData) => boolean
getConversation, // (key: string) => ConversationData | undefined
setConversations, // (list: ConversationData[]) => void
getMessages, // (key: string) => MessageInfo[] | undefined (read messages across components)
} = useXConversations({
defaultConversations: [
{ key: 'conv-1', label: 'Conversation 1' },
{ key: 'conv-2', label: 'Conversation 2' },
],
defaultActiveConversationKey: 'conv-1',
});Multi-conversation Full Pattern
import { useXChat, useXConversations } from '@ant-design/x-sdk';
import { OpenAIChatProvider, XRequest } from '@ant-design/x-sdk';
import { Bubble, Conversations, Sender } from '@ant-design/x';
import React, { useEffect, useRef } from 'react';
// ⚠️ Each conversation must have its own Provider instance, otherwise state mixes
const providerCache = new Map<string, OpenAIChatProvider>();
function getProvider(key: string): OpenAIChatProvider {
if (!providerCache.has(key)) {
providerCache.set(
key,
new OpenAIChatProvider({
request: XRequest(BASE_URL, { manual: true, params: { model: 'gpt-4o', stream: true } }),
}),
);
}
return providerCache.get(key)!;
}
const App = () => {
const senderRef = useRef<any>(null);
const { conversations, activeConversationKey, setActiveConversationKey, addConversation } =
useXConversations({
defaultConversations: [{ key: 'conv-1', label: 'New Conversation' }],
defaultActiveConversationKey: 'conv-1',
});
const { messages, onRequest, isRequesting, abort, queueRequest } = useXChat({
provider: getProvider(activeConversationKey),
conversationKey: activeConversationKey,
// Async load default messages
defaultMessages: async ({ conversationKey }) => {
// Load history from server based on conversationKey
return [];
},
requestFallback: (_, { error, messageInfo }) => {
if (error.name === 'AbortError') {
return { content: messageInfo?.message?.content || 'Cancelled', role: 'assistant' };
}
return { content: 'Request failed', role: 'assistant' };
},
});
// Clear input on conversation switch
useEffect(() => {
senderRef.current?.clear?.();
}, [activeConversationKey]);
const handleNewConversation = () => {
const newKey = `conv-${Date.now()}`;
addConversation({ key: newKey, label: `New Conversation ${conversations.length + 1}` });
setActiveConversationKey(newKey);
};
return (
<div style={{ display: 'flex', height: '100vh' }}>
<Conversations
items={conversations}
activeKey={activeConversationKey}
onActiveChange={setActiveConversationKey}
creation={{ onClick: handleNewConversation }}
/>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Bubble.List
role={{ assistant: { placement: 'start' }, user: { placement: 'end' } }}
items={messages.map(({ id, message, status }) => ({
key: id,
role: message.role,
content: message.content,
loading: status === 'loading',
}))}
/>
<Sender
ref={senderRef}
loading={isRequesting}
onCancel={abort}
onSubmit={(val) => {
onRequest({ messages: [{ role: 'user', content: val }] });
}}
/>
</div>
</div>
);
};queueRequest: Delayed Send After Conversation Switch
// Scenario: user switches to a new conversation and triggers an initial message simultaneously
// queueRequest waits for defaultMessages async loading to complete, then sends the request
const handleNewConversationWithFirstMessage = () => {
const newKey = `conv-${Date.now()}`;
addConversation({ key: newKey, label: 'New Conversation' });
setActiveConversationKey(newKey);
// Queue the message; sent automatically after newKey conversation's defaultMessages finish loading
queueRequest(newKey, {
messages: [{ role: 'user', content: 'Hello! Please introduce yourself.' }],
});
};📋 Prerequisites and Dependencies
| Usage Scenario | Required Skill/Provider | Order |
|---|---|---|
| Private API Adaptation | x-chat-provider → use-x-chat | Create Provider first |
| Standard API | Built-in Provider + use-x-chat | Direct use |
| Multi-conversation | Provider factory + useXConversations + useXChat | Use together |
🚨 Development Rules
Before using use-x-chat, confirm:
- [ ] Has Provider (custom or built-in Provider)
- [ ] Provider's XRequest is configured with
manual: true - [ ] Understands
MessageInfodata structure (message content is inmsg.message) - [ ]
Bubble.Listusesroleprop (not `roles`) - [ ] Multi-conversation scenario: each conversation has its own Provider instance
Test Case Rules
- If the user does not explicitly need test cases, do not add test files
Code Quality Rules
- After completion, must check types: Run
tsc --noEmitto ensure no type errors - Keep code clean: Remove all unused variables and imports
🔗 Reference Resources
📚 Core Reference Documentation
- API.md - Complete API reference documentation
- CORE.md - Core function details
- EXAMPLES.md - Practical example code
🌐 SDK Official Documentation
💻 Example Code
- with-x-chat.tsx - Multi-conversation full example
- openai-callback.tsx - callbacks + removeMessage example
- developer.tsx - System prompt with developer role
- custom-provider-width-ui.tsx - Custom Provider full example
useXChat
useXChat
```tsx | pure type useXChat< ChatMessage extends SimpleType = string, ParsedMessage extends SimpleType = ChatMessage, Input = RequestParams<ChatMessage>, Output = SSEOutput,
= (config: XChatConfig<ChatMessage, ParsedMessage, Input, Output>) => XChatConfigReturnType;
<!-- prettier-ignore -->
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| ChatMessage | Message data type, defines the structure of chat messages | object | object | - |
| ParsedMessage | Parsed message type, message format for component consumption | ChatMessage | ChatMessage | - |
| Input | Request parameter type, defines the structure of request parameters | RequestParams\<ChatMessage\> | RequestParams\<ChatMessage\> | - |
| Output | Response data type, defines the format of received response data | SSEOutput | SSEOutput | - |
### XChatConfig
<!-- prettier-ignore -->
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| provider | Data provider used to convert data and requests of different structures into formats that useXChat can consume. The platform includes built-in `DefaultChatProvider` and `OpenAIChatProvider`, and you can also implement your own Provider by inheriting `AbstractChatProvider`. See: [Chat Provider Documentation](/x-sdks/chat-provider) | AbstractChatProvider\<ChatMessage, Input, Output\> | - | - |
| conversationKey | Session unique identifier (globally unique), used to distinguish different sessions | string | Symbol('ConversationKey') | - |
| defaultMessages | Default display messages | MessageInfo\<ChatMessage\>[] \| (info: { conversationKey?: string }) => MessageInfo\<ChatMessage\>[] \| (info: { conversationKey?: string }) => Promise\<MessageInfo\<ChatMessage\>[]\> | - | - |
| parser | Converts ChatMessage into ParsedMessage for consumption. When not set, ChatMessage is consumed directly. Supports converting one ChatMessage into multiple ParsedMessages | (message: ChatMessage) => BubbleMessage \| BubbleMessage[] | - | - |
| requestFallback | Fallback message for failed requests. When not provided, no message will be displayed | ChatMessage \| (requestParams: Partial\<Input\>,info: { error: Error; errorInfo: any; messages: ChatMessage[], messageInfo: MessageInfo\<ChatMessage\> }) => ChatMessage\|Promise\<ChatMessage\> | - | - |
| requestPlaceholder | Placeholder message during requests. When not provided, no message will be displayed | ChatMessage \| (requestParams: Partial\<Input\>, info: { messages: Message[] }) => ChatMessage \| Promise\<Message\> | - | - |
### XChatConfigReturnType
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| abort | Cancel request | () => void | - | - |
| isRequesting | Whether a request is in progress | boolean | - | - |
| isDefaultMessagesRequesting | Whether the default message list is requesting | boolean | false | 2.2.0 |
| messages | Current managed message list content | MessageInfo\<ChatMessage\>[] | - | - |
| parsedMessages | Content translated through `parser` | MessageInfo\<ParsedMessages\>[] | - | - |
| onReload | Regenerate, will send request to backend and update the message with new returned data | (id: string \| number, requestParams: Partial\<Input\>, opts?: { extraInfo: AnyObject }) => void | - | - |
| onRequest | Add a Message and trigger request | (requestParams: Partial\<Input\>, opts?: { extraInfo: AnyObject }) => void | - | - |
| setMessages | Directly modify messages without triggering requests | (messages: Partial\<MessageInfo\<ChatMessage\>\>[]) => void | - | - |
| setMessage | Directly modify a single message without triggering requests | (id: string \| number, info: Partial\<MessageInfo\<ChatMessage\>\>) => void | - | - |
| removeMessage | Deleting a single message will not trigger a request | (id: string \| number) => boolean | - | - |
| queueRequest | Will add the request to a queue, waiting for the conversationKey to be initialized before sending | (conversationKey: string \| symbol, requestParams: Partial\<Input\>, opts?: { extraInfo: AnyObject }) => void | - | - |
#### MessageInfo
interface MessageInfo<ChatMessage> { id: number | string; message: ChatMessage; status: MessageStatus; extraInfo?: AnyObject; }
#### MessageStatus
type MessageStatus = 'local' | 'loading' | 'updating' | 'success' | 'error' | 'abort';
---
## useXConversations
### useXConversations
type useXConversations = (config: XConversationConfig) => { conversations: ConversationData[]; activeConversationKey: string; setActiveConversationKey: (key: string) => boolean; addConversation: (conversation: ConversationData, placement?: 'prepend' | 'append') => boolean; removeConversation: (key: string) => boolean; setConversation: (key: string, conversation: ConversationData) => boolean; getConversation: (key: string) => ConversationData; setConversations: (conversations: ConversationData[]) => boolean; getMessages: (conversationKey: string) => any[]; };
### XConversationConfig
interface XConversationConfig { defaultConversations?: ConversationData[]; defaultActiveConversationKey?: string; }
### ConversationData
interface ConversationData extends AnyObject { key: string; label?: string; }
1. Message Management
Get Message List
const { messages } = useXChat({ provider });
// messages structure: MessageInfo<ChatMessage>[]
// Actual message data is in msg.message
// msg.status: 'local' | 'loading' | 'updating' | 'success' | 'error' | 'abort'Manually Set Messages (no request triggered)
const { setMessages } = useXChat({ provider });
// Clear messages
setMessages([]);
// Add welcome message
setMessages([
{
id: 'welcome',
message: { content: 'Welcome to AI Assistant', role: 'assistant' },
status: 'success',
},
]);Update Single Message
const { setMessage } = useXChat({ provider });
// Update message content
setMessage('msg-id', {
message: { content: 'New content', role: 'assistant' },
});
// Mark as error status
setMessage('msg-id', { status: 'error' });
// Update with extraInfo
setMessage('msg-id', {
message: { content: 'Edited', role: 'user' },
extraInfo: { edited: true, editedAt: Date.now() },
});Delete Message
const { removeMessage } = useXChat({ provider });
// Delete first message
removeMessage(messages[0]?.id);
// Delete all error-status messages
messages.filter((m) => m.status === 'error').forEach((m) => removeMessage(m.id));2. Request Control
Send Message
const { onRequest } = useXChat({ provider });
// Basic usage (onRequest param type is Partial<Input>)
onRequest({ query: 'User question' });
// With extra metadata (extraInfo is stored in MessageInfo.extraInfo)
onRequest({ query: 'User question' }, { extraInfo: { sourceId: 'msg-123', isRetry: false } });
// For OpenAIChatProvider, send full message array
onRequest({
messages: [{ role: 'user', content: 'Question content' }],
temperature: 0.7,
});Abort Request
const { abort, isRequesting } = useXChat({ provider });
<button onClick={abort} disabled={!isRequesting}>
Stop generation
</button>;
// abort triggers requestFallback with error.name === 'AbortError'Regenerate (onReload)
const { messages, onReload, isRequesting } = useXChat({ provider });
// Add regenerate button to assistant messages
items={messages.map((msg) => ({
key: msg.id,
role: msg.message.role,
content: msg.message.content,
loading: msg.status === 'loading',
footer: msg.message.role === 'assistant' && (
<Button
size="small"
type="text"
icon={<SyncOutlined />}
loading={msg.status === 'loading' && isRequesting}
onClick={() => onReload(msg.id, {}, { extraInfo: { isRegenerate: true } })}
>
Regenerate
</Button>
),
}))}⚠️onReload's second parameter isrequestParams; usually pass{}(original context will be reused)
3. Error Handling
Unified Error Handling
const { messages } = useXChat({
provider,
requestFallback: (_, { error, errorInfo, messageInfo }) => {
// User actively cancelled
if (error.name === 'AbortError') {
return {
content: messageInfo?.message?.content || 'Reply cancelled',
role: 'assistant' as const,
};
}
// Timeout error
if (error.name === 'TimeoutError' || error.name === 'StreamTimeoutError') {
return { content: 'Request timed out, please retry later', role: 'assistant' as const };
}
// Server-returned error message
if (errorInfo?.error?.message) {
return { content: errorInfo.error.message, role: 'assistant' as const };
}
// Network error fallback
return { content: 'Network error, please retry later', role: 'assistant' as const };
},
});Async requestFallback
requestFallback: async (requestParams, { error, messageInfo }) => {
if (error.name === 'AbortError') {
return { content: messageInfo?.message?.content || 'Cancelled', role: 'assistant' };
}
// Can do async operations like error reporting
await reportError(error);
return { content: 'Error recorded, please retry later', role: 'assistant' };
},4. Default Messages and Placeholder
Synchronous Default Messages
const { messages } = useXChat({
provider,
defaultMessages: [
{ id: 'sys', message: { role: 'developer', content: 'System prompt' }, status: 'success' },
{ id: '0', message: { role: 'user', content: 'Hello' }, status: 'success' },
{
id: '1',
message: { role: 'assistant', content: 'Hello! I am an AI assistant' },
status: 'success',
},
],
});Async Default Messages (fetch history from server)
const { messages, isDefaultMessagesRequesting } = useXChat({
provider,
conversationKey: activeKey,
defaultMessages: async ({ conversationKey }) => {
const history = await fetchHistory(conversationKey);
return history.map((item, index) => ({
id: `history_${index}`,
message: { role: item.role, content: item.content },
status: 'success' as const,
}));
},
});
// isDefaultMessagesRequesting: true while async loading
if (isDefaultMessagesRequesting) {
return <Spin />;
}Custom Request Placeholder
requestPlaceholder: (requestParams, { messages }) => {
return {
content: `Generating reply (${messages.length} messages so far)...`,
role: 'assistant',
};
},5. parser: Message Format Conversion
Use parser when ChatMessage needs to be split into multiple bubbles (one-to-many):
import { useXChat } from '@ant-design/x-sdk';
// Scenario: one ChatMessage contains reasoning chain + answer, split into two bubbles
const { parsedMessages } = useXChat({
provider,
parser: (message: MyMessage) => {
if (message.reasoning && message.content) {
return [
{ content: message.reasoning, role: 'assistant', type: 'reasoning' },
{ content: message.content, role: 'assistant', type: 'answer' },
];
}
return { content: message.content, role: message.role };
},
});
// Use parsedMessages instead of messages for Bubble.List
<Bubble.List
items={parsedMessages.map(({ id, message, status }) => ({
key: id,
role: message.role,
content: message.content,
loading: status === 'loading',
}))}
/>;6. extraInfo: Message Metadata
// Attach extraInfo when sending
onRequest(
{ query: 'Hello' },
{ extraInfo: { sourceComponent: 'SearchPanel', queryId: 'q-001' } },
);
// Read extraInfo from messages
messages.map((msg) => ({
key: msg.id,
content: msg.message.content,
// extraInfo stores metadata attached at send time
'data-query-id': msg.extraInfo?.queryId,
}));
// requestFallback can use extraInfo to identify message source
requestFallback: (requestParams, { messageInfo }) => {
const isRetry = messageInfo?.extraInfo?.isRetry;
return {
content: isRetry ? 'Retry also failed' : 'Request failed',
role: 'assistant',
};
},7. developer / system Role Handling
OpenAIChatProvider supports developer and system roles as system prompts; these messages are typically not shown to users:
const { messages, setMessage } = useXChat({
provider,
defaultMessages: [
// developer role: equivalent to system prompt
{
id: 'sys',
message: { role: 'developer', content: 'You are a helpful assistant' },
status: 'success',
},
{ id: '0', message: { role: 'user', content: 'Hello' }, status: 'success' },
{ id: '1', message: { role: 'assistant', content: 'Hello!' }, status: 'success' },
],
});
// Filter out developer/system messages from display
const chatMessages = messages.filter(
(m) => m.message.role !== 'developer' && m.message.role !== 'system',
);
// Dynamically update system prompt
const updateSystemPrompt = (newPrompt: string) => {
setMessage('sys', {
message: { role: 'developer', content: newPrompt },
});
};8. Bubble.List role Configuration
⚠️ Common mistake:Bubble.Listuses theroleprop, notroles
// ✅ Correct
<Bubble.List
role={{
assistant: { placement: 'start' },
user: { placement: 'end' },
system: { variant: 'borderless' },
}}
items={...}
/>
// ❌ Wrong (roles is not a valid prop)
<Bubble.List roles={{ ... }} items={...} />The role field value in Bubble.List's items must match the keys in the role config:
items={messages.map(({ id, message, status }) => ({
key: id,
role: message.role, // 'user' | 'assistant' | 'system' — must match role config keys
content: message.content,
loading: status === 'loading',
// status can be passed in (not required but sometimes useful)
status: status,
}))}Complete Examples
1. Basic Chat (OpenAI Provider)
import React, { useRef } from 'react';
import { Bubble, Sender } from '@ant-design/x';
import { OpenAIChatProvider, useXChat, XRequest } from '@ant-design/x-sdk';
import type { XModelMessage, XModelParams, XModelResponse } from '@ant-design/x-sdk';
import XMarkdown from '@ant-design/x-markdown';
const BASE_URL = 'https://api.openai.com/v1/chat/completions';
const MODEL = 'gpt-4o';
const App = () => {
const [provider] = React.useState(
new OpenAIChatProvider({
request: XRequest<XModelParams, XModelResponse, XModelMessage>(BASE_URL, {
manual: true,
headers: { Authorization: 'Bearer your-api-key' },
params: { model: MODEL, stream: true },
}),
}),
);
const { messages, onRequest, isRequesting, abort, onReload } = useXChat({
provider,
defaultMessages: [
{ id: '0', message: { role: 'user', content: 'Hello' }, status: 'success' },
{
id: '1',
message: { role: 'assistant', content: 'Hello! How can I help you?' },
status: 'success',
},
],
requestPlaceholder: () => ({ content: 'Thinking...', role: 'assistant' }),
requestFallback: (_, { error, messageInfo }) => {
if (error.name === 'AbortError') {
return { content: messageInfo?.message?.content || 'Cancelled', role: 'assistant' };
}
return { content: 'Request failed, please retry', role: 'assistant' };
},
});
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 600 }}>
<Bubble.List
style={{ flex: 1 }}
role={{
assistant: {
placement: 'start',
contentRender(content: string) {
return <XMarkdown content={content} />;
},
},
user: { placement: 'end' },
}}
items={messages.map(({ id, message, status }) => ({
key: id,
role: message.role,
content: message.content,
loading: status === 'loading',
}))}
/>
<Sender
loading={isRequesting}
onCancel={abort}
onSubmit={(content) => {
onRequest({ messages: [{ role: 'user', content }] });
}}
/>
</div>
);
};
export default App;2. Multi-conversation Management (useXConversations + useXChat)
import React, { useEffect, useRef } from 'react';
import { Bubble, Conversations, Sender } from '@ant-design/x';
import { OpenAIChatProvider, useXChat, useXConversations, XRequest } from '@ant-design/x-sdk';
import type { XModelParams, XModelResponse } from '@ant-design/x-sdk';
import { GetRef } from 'antd';
const BASE_URL = 'https://api.openai.com/v1/chat/completions';
// Each conversation maintains its own Provider instance
const providerCache = new Map<string, OpenAIChatProvider>();
function getProvider(key: string): OpenAIChatProvider {
if (!providerCache.has(key)) {
providerCache.set(
key,
new OpenAIChatProvider({
request: XRequest<XModelParams, XModelResponse>(BASE_URL, {
manual: true,
headers: { Authorization: 'Bearer your-api-key' },
params: { model: 'gpt-4o', stream: true },
}),
}),
);
}
return providerCache.get(key)!;
}
const App = () => {
const senderRef = useRef<GetRef<typeof Sender>>(null);
const {
conversations,
activeConversationKey,
setActiveConversationKey,
addConversation,
removeConversation,
} = useXConversations({
defaultConversations: [{ key: 'conv-1', label: 'New Conversation' }],
defaultActiveConversationKey: 'conv-1',
});
const { messages, onRequest, isRequesting, abort, queueRequest } = useXChat({
provider: getProvider(activeConversationKey),
conversationKey: activeConversationKey,
// Async load history messages
defaultMessages: async ({ conversationKey }) => {
// const history = await api.getHistory(conversationKey);
return [];
},
requestFallback: (_, { error, messageInfo }) => {
if (error.name === 'AbortError') {
return { content: messageInfo?.message?.content || 'Cancelled', role: 'assistant' };
}
return { content: 'Request failed, please retry', role: 'assistant' };
},
});
// Clear input on conversation switch
useEffect(() => {
senderRef.current?.clear?.();
}, [activeConversationKey]);
const handleNewConversation = () => {
const newKey = `conv-${Date.now()}`;
addConversation({ key: newKey, label: `New Conversation ${conversations.length + 1}` });
setActiveConversationKey(newKey);
};
const handleDeleteConversation = (key: string) => {
removeConversation(key);
if (activeConversationKey === key) {
const remaining = conversations.filter((c) => c.key !== key);
if (remaining.length > 0) setActiveConversationKey(remaining[0].key);
}
};
return (
<div style={{ display: 'flex', height: '100vh' }}>
<Conversations
style={{ width: 240, borderRight: '1px solid #f0f0f0' }}
items={conversations}
activeKey={activeConversationKey}
onActiveChange={setActiveConversationKey}
creation={{ onClick: handleNewConversation }}
menu={(conv) => ({
items: [{ label: 'Delete', key: 'delete', danger: true }],
onClick: ({ key }) => key === 'delete' && handleDeleteConversation(conv.key),
})}
/>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Bubble.List
style={{ flex: 1, padding: 16 }}
role={{ assistant: { placement: 'start' }, user: { placement: 'end' } }}
items={messages.map(({ id, message, status }) => ({
key: id,
role: message.role,
content: message.content,
loading: status === 'loading',
}))}
/>
<div style={{ padding: 16, borderTop: '1px solid #f0f0f0' }}>
<Sender
ref={senderRef}
loading={isRequesting}
onCancel={abort}
onSubmit={(val) => {
onRequest({ messages: [{ role: 'user', content: val }] });
}}
/>
</div>
</div>
</div>
);
};
export default App;3. With Regenerate Feature
import React, { useRef, useState } from 'react';
import { Bubble, Sender } from '@ant-design/x';
import { SyncOutlined } from '@ant-design/icons';
import { OpenAIChatProvider, useXChat, XRequest } from '@ant-design/x-sdk';
import { Button, Tooltip, GetRef } from 'antd';
const App = () => {
const senderRef = useRef<GetRef<typeof Sender>>(null);
const [regeneratingId, setRegeneratingId] = useState<string | number | null>(null);
const [provider] = React.useState(
new OpenAIChatProvider({
request: XRequest(BASE_URL, { manual: true, params: { model: 'gpt-4o', stream: true } }),
}),
);
const { messages, onRequest, onReload, isRequesting, abort } = useXChat({
provider,
requestPlaceholder: () => ({ content: 'Thinking...', role: 'assistant' }),
requestFallback: (_, { error, messageInfo }) => {
if (error.name === 'AbortError') {
return { content: messageInfo?.message?.content || 'Cancelled', role: 'assistant' };
}
return { content: 'Request failed, please retry', role: 'assistant' };
},
});
const handleRegenerate = (id: string | number) => {
setRegeneratingId(id);
onReload(id, {}, { extraInfo: { isRegenerate: true } });
};
return (
<div>
<Bubble.List
role={{ assistant: { placement: 'start' }, user: { placement: 'end' } }}
items={messages.map(({ id, message, status }) => ({
key: id,
role: message.role,
content: message.content,
loading: status === 'loading',
footer:
message.role === 'assistant' ? (
<Tooltip title="Regenerate">
<Button
size="small"
type="text"
icon={<SyncOutlined />}
loading={regeneratingId === id && isRequesting}
disabled={isRequesting && regeneratingId !== id}
onClick={() => handleRegenerate(id)}
/>
</Tooltip>
) : undefined,
}))}
/>
<Sender
ref={senderRef}
loading={isRequesting}
onCancel={abort}
onSubmit={(val) => {
onRequest({ messages: [{ role: 'user', content: val }] });
senderRef.current?.clear?.();
}}
/>
</div>
);
};4. With System Prompt (developer role)
const App = () => {
const [provider] = React.useState(
new OpenAIChatProvider({
request: XRequest(BASE_URL, { manual: true, params: { model: 'gpt-4o', stream: true } }),
}),
);
const { messages, onRequest, setMessage, isRequesting, abort } = useXChat({
provider,
defaultMessages: [
// developer role as system prompt; OpenAIChatProvider automatically includes it in every request
{
id: 'sys',
message: {
role: 'developer',
content: 'You are a professional frontend engineer assistant',
},
status: 'success',
},
],
requestFallback: (_, { error }) => ({
content: error.name === 'AbortError' ? 'Cancelled' : 'Request failed',
role: 'assistant',
}),
});
// Filter out developer messages from display
const displayMessages = messages.filter((m) => m.message.role !== 'developer');
// Dynamically update system prompt
const updateSystemPrompt = (prompt: string) => {
setMessage('sys', { message: { role: 'developer', content: prompt } });
};
return (
<div>
<Bubble.List
role={{ assistant: { placement: 'start' }, user: { placement: 'end' } }}
items={displayMessages.map(({ id, message, status }) => ({
key: id,
role: message.role,
content: message.content,
loading: status === 'loading',
}))}
/>
<Sender
loading={isRequesting}
onCancel={abort}
onSubmit={(val) => onRequest({ messages: [{ role: 'user', content: val }] })}
/>
</div>
);
};5. Using parser (split one message into multiple bubbles)
// Scenario: DeepSeek R1's reasoning_content + content need to be displayed separately
import React from 'react';
import { Bubble, Sender } from '@ant-design/x';
import { DeepSeekChatProvider, useXChat, XRequest } from '@ant-design/x-sdk';
import type { XModelMessage } from '@ant-design/x-sdk';
interface MyMessage extends XModelMessage {
reasoning?: string; // Chain-of-thought content
}
const BASE_URL = 'YOUR_BASE_URL';
const App = () => {
const [provider] = React.useState(
new DeepSeekChatProvider({
request: XRequest(BASE_URL, {
manual: true,
params: { model: 'deepseek-reasoner', stream: true },
}),
}),
);
const { parsedMessages, onRequest, isRequesting, abort } = useXChat<
MyMessage,
{ role: string; content: string } // ParsedMessage
>({
provider,
// parser converts one message into multiple bubbles
parser: (message: MyMessage) => {
const result: { role: string; content: string }[] = [];
if (message.reasoning) {
result.push({ role: 'reasoning', content: message.reasoning });
}
if (message.content) {
result.push({ role: 'assistant', content: message.content as string });
}
return result.length > 0
? result
: { role: message.role, content: message.content as string };
},
});
// Use parsedMessages instead of messages
return (
<div>
<Bubble.List
role={{
assistant: { placement: 'start' },
user: { placement: 'end' },
reasoning: { placement: 'start', variant: 'borderless' },
}}
items={parsedMessages.map(({ id, message, status }) => ({
key: id,
role: message.role,
content: message.content,
loading: status === 'loading',
}))}
/>
<Sender
loading={isRequesting}
onCancel={abort}
onSubmit={(val) => onRequest({ messages: [{ role: 'user', content: val }] })}
/>
</div>
);
};
export default App;