@asgard-js/react
v0.2.40
Published
This package provides React components and hooks for integrating with the Asgard AI platform, allowing you to build interactive chat interfaces.
Readme
AsgardJs React
This package provides React components and hooks for integrating with the Asgard AI platform, allowing you to build interactive chat interfaces.
Installation
To install the React package, use the following command:
npm install @asgard-js/core @asgard-js/react
Usage
Basic Usage
Here's a basic example of how to use the React components:
import React, { useRef } from 'react';
import { Chatbot } from '@asgard-js/react';
const chatbotRef = useRef(null);
const App = () => {
return (
<div style={{ width: '800px', position: 'relative' }}>
<button
style={{
position: 'absolute',
top: '80px',
right: '50%',
transform: 'translateX(50%)',
zIndex: 10,
border: '1px solid white',
borderRadius: '5px',
color: 'white',
backgroundColor: 'transparent',
cursor: 'pointer',
padding: '0.5rem 1rem',
}}
onClick={() => chatbotRef.current?.serviceContext?.sendMessage?.({ text: 'Hello' })}
>
Send a message from outside of chatbot
</button>
<Chatbot
ref={chatbotRef}
title="Asgard AI Chatbot"
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
debugMode: true, // Enable to see deprecation warnings
transformSsePayload: payload => {
return payload;
},
}}
enableLoadConfigFromService={true}
customChannelId="your-channel-id"
initMessages={[]}
debugMode={false}
fullScreen={false}
avatar="https://example.com/avatar.png"
botTypingPlaceholder="Bot is typing..."
inputPlaceholder="Type your message here..."
defaultLinkTarget="_blank"
onReset={() => {
console.log('Chat reset');
}}
onClose={() => {
console.log('Chat closed');
}}
onSseMessage={(response, ctx) => {
if (response.eventType === 'asgard.run.done') {
console.log('onSseMessage', response, ctx.conversation);
setTimeout(() => {
// delay some time to wait for the serviceContext to be available
chatbotRef.current?.serviceContext?.sendMessage?.({
text: 'Say hi after 5 seconds',
});
}, 5000);
}
}}
/>
</div>
);
};
export default App;
File Upload Support
The Chatbot component includes built-in file upload capabilities for sending images. You can control this feature using the enableUpload prop.
Enabling File Upload
<Chatbot
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="your-channel-id"
enableUpload={true} // Explicitly enable file upload
/>Control via Remote Configuration
When enableLoadConfigFromService is enabled, you can also control the upload feature through the bot provider's embedConfig:
<Chatbot
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="your-channel-id"
enableLoadConfigFromService={true}
// Upload feature will be controlled by annotations.embedConfig.enableUpload from the API
/>Configuration Priority (highest to lowest):
enableUploadprop valueannotations.embedConfig.enableUploadfrom bot provider metadata- Default:
false
Features: Multiple file selection, image preview with modal view, and responsive design. Supports JPEG, PNG, GIF, WebP up to 20MB per file, maximum 10 files at once.
Conversation Export
The Chatbot component includes built-in conversation export functionality, allowing users to download chat history as Markdown files. You can control this feature using the enableExport prop.
Enabling Conversation Export
<Chatbot
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="your-channel-id"
enableExport={true} // Explicitly enable conversation export
/>Control via Remote Configuration
When enableLoadConfigFromService is enabled, you can also control the export feature through the bot provider's embedConfig:
<Chatbot
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="your-channel-id"
enableLoadConfigFromService={true}
// Export feature will be controlled by annotations.embedConfig.enableExport from the API
/>Configuration Priority (highest to lowest):
enableExportprop valueannotations.embedConfig.enableExportfrom bot provider metadata- Default:
false
Features: Download button in chatbot footer, exports conversation history with timestamps and trace IDs, human-readable filename format ({BotName}_對話紀錄_{Date}_{Time}.md).
API Key Authentication
For applications that need dynamic API key input (such as embedded chatbots), you can use the authentication state management:
import React, { useState } from 'react';
import { Chatbot } from '@asgard-js/react';
import { AuthState } from '@asgard-js/core';
const EmbedApp = () => {
const [authState, setAuthState] = useState < AuthState > 'needApiKey';
const handleApiKeySubmit = async (apiKey: string) => {
setAuthState('loading');
try {
// Validate the API key (implement your validation logic)
const isValid = await validateApiKey(apiKey);
setAuthState(isValid ? 'authenticated' : 'invalidApiKey');
} catch (error) {
setAuthState('error');
}
};
return (
<Chatbot
title="Asgard AI Assistant"
authState={authState}
onApiKeySubmit={handleApiKeySubmit}
config={{
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
// Note: Don't set apiKey here when using dynamic authentication
}}
customChannelId="embed-channel"
fullScreen={false}
/>
);
};Migration from endpoint to botProviderEndpoint
Important: The endpoint configuration option is deprecated. Use botProviderEndpoint instead for simplified configuration.
Before (Deprecated)
config: {
apiKey: 'your-api-key',
endpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}/message/sse',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}After (Recommended)
config: {
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
// SSE endpoint is automatically derived as: botProviderEndpoint + '/message/sse'
}Benefits:
- Simplified configuration with single endpoint
- Reduced chance of configuration errors
- Automatic endpoint derivation
Backward Compatibility: Existing code using endpoint will continue to work but may show deprecation warnings when debugMode is enabled.
Migration from endpoint to botProviderEndpoint
API Reference
Chatbot Component Props
- title?:
string- The title of the chatbot (optional). If not provided, will use the value from the API if available. - config:
ClientConfig- Configuration object for the Asgard service client, including:apiKey?:string(optional) - API key for authentication. Can be omitted when using dynamic authenticationbotProviderEndpoint:string(required) - Bot provider endpoint URL (SSE endpoint will be auto-derived)endpoint?:string(deprecated) - Legacy API endpoint URL. UsebotProviderEndpointinstead.transformSsePayload?:(payload: FetchSsePayload) => FetchSsePayload- SSE payload transformercustomHeaders?:Record<string, string>- Custom headers to include in SSE and API requests (e.g., Bearer token viaAuthorizationheader)userIdentityHint?:string- Optional user identity hint. When provided, all requests will include theX-ASGARD-USER-IDENTITY-HINTheader with this valuedebugMode?:boolean- Enable debug mode, defaults tofalseonRunInit?:InitEventHandler- Handler for run initialization eventsonMessage?:MessageEventHandler- Handler for message eventsonToolCall?:ToolCallEventHandler- Handler for tool call events. See Tool Call Handler section for details.onProcess?:ProcessEventHandler- Handler for process eventsonRunDone?:DoneEventHandler- Handler for run completion eventsonRunError?:ErrorEventHandler- Error handler for execution errors
- customActions?:
ReactNode[]- Custom actions to display on the chatbot header - enableLoadConfigFromService?:
boolean- Enable loading configuration from service - enableUpload?:
boolean- Enable file upload functionality. When set, it takes priority over theembedConfig.enableUploadsetting from the bot provider metadata. Defaults tofalseif not specified in either location. Supports image files (JPEG, PNG, GIF, WebP) up to 20MB per file, maximum 10 files at once. - enableExport?:
boolean- Enable conversation export functionality. When set, it takes priority over theembedConfig.enableExportsetting from the bot provider metadata. Defaults tofalseif not specified in either location. Adds a download button to the chatbot footer that exports the conversation history as a Markdown file with timestamps and trace IDs. - maintainConnectionWhenClosed?:
boolean- Maintain connection when chat is closed, defaults tofalse - loadingComponent?:
ReactNode- Custom loading component - asyncInitializers?:
Record<string, () => Promise<unknown>>- Asynchronous initializers for app initialization before rendering any component. Good for loading data or other async operations as the initial state. It only works whenenableLoadConfigFromServiceis set totrue. - customChannelId:
string- Custom channel identifier for the chat session - initMessages:
ConversationMessage[]- Initial messages to display in the chat - fullScreen:
boolean- Display chatbot in full screen mode, defaults tofalse - avatar:
string- URL for the chatbot's avatar image - botTypingPlaceholder:
string- Text to display while the bot is typing - inputPlaceholder:
string- Custom placeholder text for the message input field - defaultLinkTarget?:
'_blank' | '_self' | '_parent' | '_top'- Default target for opening URIs when not specified by the API. Defaults to'_blank'(opens in new tab). - theme:
Partial<AsgardThemeContextValue>- Custom theme configuration - autoResetChannel?:
boolean- Whether to automatically reset channel on mount. Defaults totrue. When set tofalse, the channel is created without sendingRESET_CHANNEL, preserving history messages loaded viainitMessages. See Auto Reset Channel section for details. - userIdentityHint?:
string- Optional user identity hint. When provided, all requests (SSE and file upload) will include theX-ASGARD-USER-IDENTITY-HINTheader with this value. - onMessageSent?:
() => void- Callback fired after a message is successfully sent. Useful for tracking message count or triggering side effects. - onChannelReady?:
() => void- Callback fired once the chat channel is ready to accept messages. Use this instead of polling forsendMessageavailability when you need to send an initial message right after the chatbot mounts. Re-fires after channel reset. See On Channel Ready section for details. - onReset:
() => void- Callback function when chat is reset - onClose:
() => void- Callback function when chat is closed - authState?:
AuthState- Authentication state for dynamic API key management. Available states:'loading','needApiKey','authenticated','error','invalidApiKey' - onApiKeySubmit?:
(apiKey: string) => Promise<void>- Callback function when user submits API key for authentication - onTemplateBtnClick?:
(payload: Record<string, unknown>, eventName: string, raw: string) => void- Callback for EMIT button actions. See EMIT Action section for details. - messageActions?:
(message: ConversationBotMessage) => MessageActionConfig[]- Function to define which action buttons to display for each bot message. Returns an array of{ id: string, label: string }objects. See Message Actions section for details. - onMessageAction?:
(actionId: string, message: ConversationBotMessage) => void- Callback when a message action button is clicked. Receives the action ID and the associated bot message. - renderHeader?:
() => ReactNode- Custom header renderer. When provided, completely replaces the default header. UseuseAsgardContext()inside the render function to accessresetChannel,isResetting, and other internal state. - renderMenu?:
() => ReactNode- Custom menu renderer. When provided, renders content between the chat body and footer. Useful for quick menus, suggested questions, or navigation panels. See Custom Menu section for details. - footerEndActions?:
ReactNode[]- Extra action nodes rendered at the end of the footer input row, after the send/mic button. Pure additive slot — built-in textarea / attachment buttons / send / mic remain unchanged. See Footer End Actions section for details. - renderMessageContent?:
(props: MessageContentRendererProps) => ReactNode- Custom renderer for message content. Allows customizing how messages are rendered based on message properties. See Custom Message Renderer section for details. - renderToolCallGroup?:
(props: ToolCallGroupRendererProps) => ReactNode- Custom renderer for tool call group. Returnnullto hide, return JSX to fully customize, or callrenderDefaultContent()to use the default UI with optional overrides (e.g.,renderDefaultContent({ title: 'AI is thinking...' })). See Tool Call Group Renderer section for details. - onBeforeSendMessage?:
(params: SendMessageParams) => SendMessageParams- Callback to modify message params before sending. Allows injecting contextual data (payload, metadata) from parent components. See Before Send Message Hook section for details. - onSseMessage:
(response: SseResponse, ctx: AsgardServiceContextValue) => void- Callback function when SSE message is received. It would be helpful if using with the ref to provide some context and conversation data and do some proactively actions like sending messages to the bot. - ref:
ForwardedRef<ChatbotRef>- Forwarded ref to access the chatbot instance. It can be used to access the chatbot instance and do some actions like sending messages to the bot.ChatbotRefprovidesserviceContextfor interacting with the chatbot, andsetInputValue(value: string)for programmatically setting the textarea text from outside the component.
Theme Configuration
The theme configuration can be obtained from the bot provider metadata of annotations field and theme props.
The priority of themes is as follows (high to low):
- Theme from props
- Theme from annotations from bot provider metadata
- Default theme
Theme Interface
export interface AsgardThemeContextValue {
chatbot: Pick<
CSSProperties,
| 'width'
| 'height'
| 'maxWidth'
| 'minWidth'
| 'maxHeight'
| 'minHeight'
| 'backgroundColor'
| 'borderColor'
| 'borderRadius'
> & {
contentMaxWidth?: CSSProperties['maxWidth'];
backgroundColor?: CSSProperties['backgroundColor'];
borderColor?: CSSProperties['borderColor'];
inactiveColor?: CSSProperties['color'];
primaryComponent?: {
mainColor?: CSSProperties['color'];
secondaryColor?: CSSProperties['color'];
};
style?: CSSProperties;
header?: Partial<{
style: CSSProperties;
title: {
style: CSSProperties;
};
actionButton?: {
style: CSSProperties;
};
}>;
body?: Partial<{
style: CSSProperties;
}>;
footer?: Partial<{
style: CSSProperties;
textArea: {
style: CSSProperties;
'::placeholder': CSSProperties;
};
submitButton: {
style: CSSProperties;
};
speechInputButton: {
style: CSSProperties;
};
}>;
};
botMessage: Pick<CSSProperties, 'color' | 'backgroundColor'>;
userMessage: Pick<CSSProperties, 'color' | 'backgroundColor'>;
template?: Partial<{
/**
* first level for common/shared properties.
* Check MessageTemplate type for more details (packages/core/src/types/sse-response.ts).
*/
quickReplies?: Partial<{
style: CSSProperties;
button: {
style: CSSProperties;
};
}>;
references?: Partial<{
style: CSSProperties;
title?: {
style: CSSProperties;
};
item?: {
style: CSSProperties;
};
}>;
time?: Partial<{
style: CSSProperties;
}>;
TextMessageTemplate: Partial<{ style: CSSProperties }>;
HintMessageTemplate: Partial<{ style: CSSProperties }>;
ImageMessageTemplate: Partial<{ style: CSSProperties }>;
ChartMessageTemplate: Partial<{ style: CSSProperties }>;
ButtonMessageTemplate: Partial<{
style: CSSProperties;
button?: {
style: CSSProperties;
};
}>;
CarouselMessageTemplate: Partial<{
style: CSSProperties;
card: {
style: CSSProperties;
button?: {
style: CSSProperties;
};
};
}>;
// Didn't implement yet
VideoMessageTemplate: Partial<{ style: CSSProperties }>;
AudioMessageTemplate: Partial<{ style: CSSProperties }>;
LocationMessageTemplate: Partial<{ style: CSSProperties }>;
}>;
}Default Theme
The default theme uses CSS variables for consistent styling:
const defaultTheme = {
chatbot: {
width: '375px',
height: '640px',
backgroundColor: 'var(--asg-color-bg)',
borderColor: 'var(--asg-color-border)',
borderRadius: 'var(--asg-radius-md)',
contentMaxWidth: '1200px',
style: {},
header: {
style: {},
title: {
style: {},
},
actionButton: {
style: {},
},
},
body: {
style: {},
},
footer: {
style: {},
textArea: {
style: {},
'::placeholder': {
color: 'var(--asg-color-text-placeholder)',
},
},
submitButton: {
style: {},
},
speechInputButton: {
style: {},
},
},
},
botMessage: {
color: 'var(--asg-color-text)',
backgroundColor: 'var(--asg-color-secondary)',
},
userMessage: {
color: 'var(--asg-color-text)',
backgroundColor: 'var(--asg-color-primary)',
},
template: {
quickReplies: {
style: {},
button: {
style: {},
},
},
references: {
style: {},
title: {
style: {},
},
item: {
style: {},
},
},
time: {
style: {},
},
TextMessageTemplate: {
style: {},
},
HintMessageTemplate: {
style: {},
},
ImageMessageTemplate: {
style: {},
},
VideoMessageTemplate: {
style: {},
},
AudioMessageTemplate: {
style: {},
},
LocationMessageTemplate: {
style: {},
},
ChartMessageTemplate: {
style: {},
},
ButtonMessageTemplate: {
style: {},
button: {
style: {
border: '1px solid var(--asg-color-border)',
},
},
},
CarouselMessageTemplate: {
style: {},
card: {
style: {},
button: {
style: {
border: '1px solid var(--asg-color-border)',
},
},
},
},
},
};Usage Example
const App = () => {
const customTheme = {
chatbot: {
width: '400px',
height: '600px',
backgroundColor: '#ffffff',
borderRadius: '12px',
},
botMessage: {
backgroundColor: '#f0f0f0',
},
userMessage: {
backgroundColor: '#007bff',
color: '#ffffff',
},
};
return (
<Chatbot
// ... other props
theme={customTheme}
/>
);
};Note: When fullScreen prop is set to true, the chatbot's width and height will be set to 100vw and 100vh respectively, and borderRadius will be set to zero, regardless of theme settings.
Event Handlers
On Channel Ready
The onChannelReady callback fires once the chat channel is fully initialized and ready to accept messages. This is useful when you need to send an initial message right after the chatbot mounts — for example, when seeding a chat with a SQL statement carried over from another page.
Why not call sendMessage from a useEffect?
Channel initialization happens asynchronously after mount. The serviceContext.sendMessage exposed via ref becomes a function before the underlying channel is actually ready, so calling it too early results in a silent no-op. onChannelReady solves this by guaranteeing the callback fires only after the channel can receive messages.
Behavior
- Fires once after the channel is created and the imperative ref has been updated
- Re-fires after a manual
resetChannel(channel transitions from non-null → null → non-null) - Inside the callback,
ref.current.serviceContext.sendMessageis guaranteed to be a working function bound to the new channel
Usage Example
import { Chatbot, ChatbotRef } from '@asgard-js/react';
import { useCallback, useRef } from 'react';
function MyChatbot({ initialText }: { initialText?: string }) {
const chatbotRef = useRef<ChatbotRef>(null);
const handleChannelReady = useCallback(() => {
if (initialText) {
chatbotRef.current?.serviceContext?.sendMessage?.({
text: initialText,
blobIds: [],
});
}
}, [initialText]);
return (
<Chatbot
ref={chatbotRef}
config={{ botProviderEndpoint: '...' }}
customChannelId="my-channel"
onChannelReady={handleChannelReady}
/>
);
}Single-fire vs. Re-fire
If you want the work to happen only on the first ready (not after subsequent resets), use a ref guard in your callback:
const firedRef = useRef(false);
const handleChannelReady = useCallback(() => {
if (firedRef.current) return;
firedRef.current = true;
// do one-time work here
}, []);
Tool Call Handler
The onToolCall callback allows you to handle tool call events from the bot. This handler is triggered when the bot starts or completes executing a tool call. See the Tool Call Start documentation and Tool Call Complete documentation for details.
The callback receives a SseResponse object with one of the following event types:
EventType.TOOL_CALL_START: Fired when a tool call begins executionEventType.TOOL_CALL_COMPLETE: Fired when a tool call completes execution
The response object contains the following data:
For TOOL_CALL_START:
processId:string- Process identifiercallSeq:number- Call sequence numbertoolCall: Object containing:toolsetName:string- Name of the toolsettoolName:string- Name of the toolparameter:Record<string, unknown>- Tool call parameters
For TOOL_CALL_COMPLETE:
- All fields from
TOOL_CALL_STARTplus: toolCallResult: Object containing:data:unknown- Result data returned from the tool executionerror:unknown- Error information if the tool call failederrorCode:string | null- Error code if the tool call failedisSuccess:boolean- Whether the tool call succeededpaging:unknown- Paging information if applicable
Example SSE response for TOOL_CALL_COMPLETE:
{
"eventType": "asgard.tool_call.complete",
"requestId": "295fcef49f270b06e6d53f6fb3656b0c",
"eventId": "1947548755242782720",
"namespace": "proj-4b2b31bb-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"botProviderName": "bp-reviewbot-f96def0f-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"customChannelId": "syDHHkS6cQMdAWTu3T2N2X",
"fact": {
"runInit": null,
"runDone": null,
"runError": null,
"processStart": null,
"processComplete": null,
"messageStart": null,
"messageDelta": null,
"messageComplete": null,
"toolCallStart": null,
"toolCallComplete": {
"processId": "f627cac52c576dc4",
"callSeq": 0,
"toolCall": {
"toolsetName": "ts-callool-4b2b31bb-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"toolName": "movie_search",
"parameter": {}
},
"toolCallResult": {
"data": null,
"error": null,
"errorCode": null,
"isSuccess": true,
"paging": null
}
}
}
}Usage Example
import { EventType, SseResponse } from '@asgard-js/core';
const handleToolCall = (response: SseResponse<EventType.TOOL_CALL_START | EventType.TOOL_CALL_COMPLETE>): void => {
if (response.eventType === EventType.TOOL_CALL_COMPLETE) {
const { processId, callSeq, toolCall, toolCallResult } = response.fact.toolCallComplete;
console.log(`Tool call completed: ${toolCall.toolsetName}.${toolCall.toolName}`);
if (toolCallResult.isSuccess) {
console.log('Tool call succeeded. Data:', toolCallResult.data);
// Process successful results
} else {
console.error('Tool call failed:', toolCallResult.error);
console.error('Error code:', toolCallResult.errorCode);
// Handle errors
}
// You can process results, update UI, or trigger follow-up actions
}
};
// Pass the handler to Chatbot config
<Chatbot
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
onToolCall: handleToolCall,
}}
customChannelId="your-channel-id"
/>;
Tool Call Consent
When a bot provider is configured with consent-required toolsets, the SDK automatically surfaces an approval modal before each tool call executes. No additional wiring is needed — ToolCallConsentGate is mounted inside <Chatbot> automatically.
How it works
When the backend emits an asgard.tool_call.consent event, a modal appears for each pending tool call in sequence. The user chooses one of three actions:
| Action | Behaviour | | ----------------------- | -------------------------------------------------------------------------------------------------- | | Allow for This Chat | Approves this call and auto-approves all future calls to the same tool in the current chat session | | Allow Once | Approves this call only | | Deny | Rejects this call; the user may optionally provide a reason |
Two auto-skip rules reduce interruptions:
alreadyAllowed: If the backend marks a call as already allowed (e.g. from a prior "Allow for This Chat" in a previous turn), the SDK silently approves it without showing a modal.- Same-session Allow for This Chat: If the user approved a tool via "Allow for This Chat" earlier in the same consent batch, subsequent calls to that tool in the same batch are auto-approved.
Zero-config usage
// No consent-specific props needed — just point to a consent-enabled bot provider
<Chatbot
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="your-channel-id"
/>Theming
The consent modal inherits the chatbot's design tokens (--asg-color-*) automatically, so it follows the active theme without extra configuration. You can also override individual modal colours via CSS variables on any ancestor element:
.my-chatbot-wrapper {
--asgard-consent-modal-bg: #0f172a;
--asgard-consent-modal-accent: #6366f1;
--asgard-consent-modal-danger: #ef4444;
}
EMIT Action
EMIT buttons allow you to handle custom actions in your application. Implement the onTemplateBtnClick callback to process these events. See the EMIT Action documentation for details.
The callback receives the following parameters:
payload(optional): Custom data from the button actioneventName(required): Event name specified in the button actionraw(required): Complete SSE response data as JSON string. Use this when you need information beyondpayloadandeventName. Parse it to access additional fields from the original SSE response. See SSE Response documentation for the complete response structure.
Configure EMIT buttons in your backend SSE response:
{
"template": {
"type": "BUTTON",
"title": "Action Menu",
"text": "Please select an action:",
"buttons": [
{
"label": "Support Request",
"action": {
"type": "EMIT",
"eventName": "support_request",
"payload": {
"category": "technical",
"priority": "high"
}
}
}
]
}
}Usage Example
const handleTemplateBtnClick = (payload: Record<string, unknown>, eventName: string, raw: string): void => {
if (eventName === 'support_request') {
// Access payload data
const category = payload.category as string;
const priority = payload.priority as string;
// Optionally parse raw SSE data to access additional fields
let customChannelId: string | undefined;
try {
const sseData = JSON.parse(raw);
customChannelId = sseData.customChannelId;
} catch {
// Handle parse error if needed
}
const channelInfo = customChannelId ? `\nChannel ID: ${customChannelId}` : '';
window.alert(`Support request created\n\nCategory: ${category}\nPriority: ${priority}${channelInfo}`);
}
};
// Pass the handler to Chatbot
<Chatbot config={config} customChannelId={nanoid()} onTemplateBtnClick={handleTemplateBtnClick} />;
Message Actions
Message Actions allow you to add custom action buttons to bot messages. This is useful for implementing features like "Save as Topic", "Copy", "Share", or any other custom actions on individual messages.
The messageActions prop is a function that receives a bot message and returns an array of action configurations. The onMessageAction callback is triggered when a user clicks on an action button.
MessageActionConfig Interface
interface MessageActionConfig {
/** Unique identifier for the action */
id: string;
/** Display label for the action button */
label: string;
}Usage Example
import { useCallback } from 'react';
import { ConversationBotMessage } from '@asgard-js/core';
const App = () => {
const handleMessageAction = useCallback((actionId: string, message: ConversationBotMessage) => {
if (actionId === 'save-topic') {
const content = message.message.text;
console.log('Save as topic:', content);
// Implement your save logic here
} else if (actionId === 'copy') {
navigator.clipboard.writeText(message.message.text);
alert('Copied to clipboard!');
}
}, []);
return (
<Chatbot
config={config}
customChannelId="your-channel-id"
messageActions={message => {
// Return different actions based on message content or type
return [
{ id: 'save-topic', label: 'Save as Topic' },
{ id: 'copy', label: 'Copy' },
];
}}
onMessageAction={handleMessageAction}
/>
);
};Conditional Actions
You can return different actions based on the message content:
messageActions={(message) => {
const actions = [{ id: 'copy', label: 'Copy' }];
// Only show "Save as Topic" for longer messages
if (message.message.text.length > 100) {
actions.push({ id: 'save-topic', label: 'Save as Topic' });
}
return actions;
}}
Tool Call Group Renderer
The renderToolCallGroup prop allows you to customize or hide the "Answer preparation steps" UI that appears when the bot calls tools before responding.
ToolCallGroupRendererProps Interface
interface ToolCallGroupRendererProps {
/** Tool call items in the group */
items: ToolCallItemData[];
/** Timestamp of the first tool call */
time?: Date;
/** Function to render the default tool call group UI. Accepts optional overrides. */
renderDefaultContent: (overrides?: { title?: string }) => ReactNode;
}
interface ToolCallItemData {
id: string;
label: string;
status: 'pending' | 'completed' | 'error';
initial?: Record<string, unknown>;
result?: Record<string, unknown>;
}Hide completely
<Chatbot
renderToolCallGroup={() => null}
...
/>Custom title
<Chatbot
renderToolCallGroup={({ renderDefaultContent }) =>
renderDefaultContent({ title: 'AI is thinking...' })
}
...
/>Custom UI
<Chatbot
renderToolCallGroup={({ items }) => {
const done = items.filter(i => i.status === 'completed').length;
return (
<div>
{done === items.length
? `✅ ${done} steps completed`
: `⏳ Processing...`}
</div>
);
}}
...
/>
Custom Message Renderer
The renderMessageContent prop allows you to customize how messages are rendered based on message type, payload, or other conditions. This is useful for implementing custom message cards, special UI treatments, or integrating with your application's design system.
MessageContentRendererProps Interface
interface MessageContentRendererProps {
/** The original message object */
message: ConversationMessage;
/** Function to render the default message content */
renderDefaultContent: () => ReactNode;
/** Container component that wraps custom content with Avatar for bot messages */
MessageContainer: React.FC<{ children: ReactNode }>;
}Why MessageContainer?
When you use renderMessageContent to customize rendering, it completely replaces the default Template. This means Avatar will not display automatically, because Avatar is part of the default Template.
Use MessageContainer to wrap your custom content and automatically get:
- Bot messages: Avatar + timestamp
- User messages: Proper right-aligned styling
Understanding payload
The payload is custom data set by the backend Bot Provider when responding to messages. The SDK passes it directly to renderMessageContent without modification.
Backend response example (Bot Provider):
{
"template": { "type": "text", "text": "Here is your order" },
"payload": {
"customType": "order_card",
"orderId": "#ORD-2024-001234",
"items": [{ "name": "iPhone 15 Pro", "price": 42900 }]
}
}Frontend renders based on payload:
const payload = message.message.payload as { customType?: string };
if (payload?.customType === 'order_card') {
return <OrderCard order={payload} />;
}Note:
customTypeis a convention, not a requirement. You can define your own payload structure - just ensure the frontend and backend use the same format.
Basic Usage
import { Chatbot, MessageContentRendererProps } from '@asgard-js/react';
<Chatbot
config={config}
customChannelId="your-channel-id"
renderMessageContent={props => {
const { message, renderDefaultContent, MessageContainer } = props;
// Customize bot messages with specific payload types
if (message.type === 'bot') {
const payload = message.message.payload as { customType?: string };
if (payload?.customType === 'order_card') {
// Use MessageContainer to wrap custom content with Avatar
return (
<MessageContainer>
<OrderCard order={payload} />
</MessageContainer>
);
}
}
// Use default rendering for all other messages
return renderDefaultContent();
}}
/>;Using MessageContainer
The MessageContainer component is essential for maintaining consistent styling with the default messages:
- For bot messages: Wraps your content with the bot's Avatar and proper message styling (including timestamp and quick replies)
- For user messages: Applies proper right-aligned styling
- For other message types: Returns children directly
With MessageContainer (recommended for custom bot messages):
renderMessageContent={(props) => {
const { message, MessageContainer } = props;
if (message.type === 'bot' && isCustomMessage(message)) {
return (
<MessageContainer>
<MyCustomComponent data={message.message.payload} />
</MessageContainer>
);
}
return props.renderDefaultContent();
}}Without MessageContainer (when you need full control):
renderMessageContent={(props) => {
const { message } = props;
if (message.type === 'bot' && isSpecialMessage(message)) {
// Render completely custom layout without Avatar
return <FullWidthBanner data={message.message.payload} />;
}
return props.renderDefaultContent();
}}Wrapper Pattern
You can also wrap the default content to add additional elements:
renderMessageContent={(props) => {
const { message, renderDefaultContent } = props;
return (
<div className="message-wrapper" data-type={message.type}>
<div className="timestamp">{new Date().toLocaleTimeString()}</div>
{renderDefaultContent()}
<div className="message-footer">
<span>Type: {message.type}</span>
</div>
</div>
);
}}Custom User Messages
You can also customize user messages:
renderMessageContent={(props) => {
const { message, renderDefaultContent, MessageContainer } = props;
if (message.type === 'user') {
return (
<MessageContainer>
<div className="custom-user-message">
<span className="user-badge">YOU</span>
<div className="user-content">{message.text}</div>
</div>
</MessageContainer>
);
}
return renderDefaultContent();
}}
Before Send Message Hook
The onBeforeSendMessage prop allows you to modify message parameters before they are sent. This is useful for injecting contextual data from parent components into every message.
SendMessageParams Interface
interface SendMessageParams {
text: string;
blobIds?: string[];
filePreviewUrls?: string[];
documentNames?: string[];
payload?: Record<string, unknown> | (() => Record<string, unknown>);
}Use Case
When the Chatbot component is embedded in a page that has contextual information (e.g., selected category, current step, user preferences), you can inject this context into every message so the backend can provide more relevant responses.
Usage Example
import { useState, useCallback } from 'react';
import { Chatbot, SendMessageParams } from '@asgard-js/react';
const TopicCreatePage = () => {
const [selectedCategory, setSelectedCategory] = useState<{ id: string; name: string } | null>(null);
const handleBeforeSendMessage = useCallback(
(params: SendMessageParams): SendMessageParams => {
if (selectedCategory) {
return {
...params,
payload: {
categoryId: selectedCategory.id,
categoryName: selectedCategory.name,
currentPage: 'topics/create',
},
};
}
return params;
},
[selectedCategory],
);
return (
<div>
{/* Category selector */}
<select
onChange={e => setSelectedCategory({ id: e.target.value, name: e.target.options[e.target.selectedIndex].text })}
>
<option value="tech">Technology</option>
<option value="lifestyle">Lifestyle</option>
</select>
{/* Chatbot with context injection */}
<Chatbot config={config} customChannelId="topic-create-chat" onBeforeSendMessage={handleBeforeSendMessage} />
</div>
);
};Backend Integration
The injected payload will be included in the SSE request body, allowing your backend to access contextual information:
{
"text": "Help me write about AI",
"payload": {
"categoryId": "tech",
"categoryName": "Technology",
"currentPage": "topics/create"
}
}
Custom Header
The renderHeader prop allows you to completely replace the default chatbot header with your own implementation. Combined with onMessageSent and onReset, you can build features like a session message counter.
Use useAsgardContext() inside your custom header to access internal state such as resetChannel and isResetting.
Usage Example
import { useState } from 'react';
import { Chatbot, useAsgardContext } from '@asgard-js/react';
function CustomHeader({ count, onClose }: { count: number; onClose: () => void }) {
const { resetChannel, isResetting } = useAsgardContext();
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontWeight: 600 }}>My Chatbot</span>
<span style={{ fontSize: '12px' }}>Messages: {count}</span>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => {
if (!isResetting) resetChannel?.();
}}
>
Reset
</button>
<button onClick={onClose}>Close</button>
</div>
</div>
);
}
const App = () => {
const [count, setCount] = useState(0);
return (
<Chatbot
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="your-channel-id"
onMessageSent={() => setCount(c => c + 1)}
onReset={() => setCount(0)}
renderHeader={() => <CustomHeader count={count} onClose={() => console.log('closed')} />}
/>
);
};
Footer End Actions
The footerEndActions prop appends extra nodes at the end of the footer input row, after the send/mic button. Pure additive slot — the built-in textarea, attachment buttons, send button, mic button, drag-and-drop, IME composition guard, image preview, and document upload all stay untouched.
Use this for actions that complement the send flow but aren't replacements for it — for example a button that opens a fullscreen SQL editor modal, a "switch to voice mode" toggle, or a settings popover.
Usage Example
import { useRef, useState } from 'react';
import { Chatbot, ChatbotRef } from '@asgard-js/react';
const App = () => {
const chatbotRef = useRef<ChatbotRef>(null);
const [editorOpen, setEditorOpen] = useState(false);
return (
<>
<Chatbot
ref={chatbotRef}
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="your-channel-id"
footerEndActions={[
<button key="sql" onClick={() => setEditorOpen(true)} title="Open SQL editor">
SQL
</button>,
]}
/>
{editorOpen && (
<SqlEditorModal
onSubmit={text => {
chatbotRef.current?.serviceContext?.sendMessage?.({ text });
setEditorOpen(false);
}}
onCancel={() => setEditorOpen(false)}
/>
)}
</>
);
};Notes
- Items render at the trailing end of the footer input row (after send/mic). They do not participate in the attachment-buttons collapse-into-
+-menu logic on the leading side. - Each item is a plain
ReactNode; the consumer fully controls styling. Match the height of the built-in send/mic button (~36px) so the row stays visually consistent. - Avoid passing many items — the row width is finite and overflow will squeeze the textarea.
- Use
chatbotRef.current?.serviceContext?.sendMessage(oruseAsgardContext()inside a wrapper component) to send messages from the consumer-owned UI that the action triggers (e.g. modal Submit button). - Distinct from
customActions(header trailing):customActionsappends to the header title bar,footerEndActionsappends to the footer input row.
Custom Menu
The renderMenu prop renders custom content between the chat body and footer. Combined with ref.setInputValue, you can build interactive menus that fill the input on click.
Usage Example
import { useRef } from 'react';
import { Chatbot, ChatbotRef } from '@asgard-js/react';
const App = () => {
const chatbotRef = useRef<ChatbotRef>(null);
const questions = ['What services do you offer?', 'How do I get started?'];
return (
<Chatbot
ref={chatbotRef}
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="your-channel-id"
renderMenu={() => (
<div style={{ padding: '8px 12px', borderTop: '1px solid #eee' }}>
<p style={{ fontSize: 12, fontWeight: 600, color: '#888' }}>Suggested questions</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{questions.map(q => (
<button
key={q}
onClick={() => chatbotRef.current?.setInputValue?.(q)}
style={{
padding: '6px 12px',
borderRadius: 16,
border: '1px solid #ddd',
background: '#fff',
cursor: 'pointer',
}}
>
{q}
</button>
))}
</div>
</div>
)}
/>
);
};
Auto Reset Channel
By default, the Chatbot sends a RESET_CHANNEL action on mount, which resets the server-side channel state and clears previous conversation history. Set autoResetChannel={false} to skip this reset, allowing you to load history messages via initMessages and continue the conversation.
Behavior Comparison
| | autoResetChannel={true} (default) | autoResetChannel={false} |
| -------------------- | ---------------------------------------------- | ----------------------------------- |
| On mount | Sends RESET_CHANNEL via SSE | Creates channel without SSE request |
| Server state | Channel is reset, server sends welcome message | Channel state is preserved |
| Display | initMessages + server welcome message | Only initMessages (history) |
| First SSE connection | Immediately on mount | When user sends the first message |
| Header reset button | Works (calls resetChannel()) | Still works (manual reset) |
Usage Example
import { Chatbot } from '@asgard-js/react';
import { ConversationMessage } from '@asgard-js/core';
const AgentHub = () => {
// Load history messages from your backend
const [historyMessages, setHistoryMessages] = useState<ConversationMessage[]>([]);
useEffect(() => {
fetchChatHistory().then(setHistoryMessages);
}, []);
return (
<Chatbot
config={{
apiKey: 'your-api-key',
botProviderEndpoint: 'https://api.asgard-ai.com/ns/{namespace}/bot-provider/{botProviderId}',
}}
customChannelId="agent-hub-channel"
autoResetChannel={false}
initMessages={historyMessages}
/>
);
};
Development
To develop the React package locally, follow these steps:
Clone the repository and navigate to the project root directory.
Install dependencies:
npm install- Start development:
You can use the following commands to work with the React package:
# Lint the React package
npm run lint:react
# Build the package
npm run build:react
# Watch mode for development
npm run watch:reactSetup your npm registry token for npm publishing:
cd ~/
touch .npmrc
echo "//registry.npmjs.org/:_authToken={{YOUR_TOKEN}}" >> .npmrcFor working with both core and React packages:
# Lint both packages
npm run lint:packages
# Build core package (required for React package)
npm run build:core
npm run build:react
# Release packages
npm run release:core # Release core package
npm run release:react # Release React packageAll builds will be available in the dist directory of their respective packages.
Contributing
We welcome contributions! Please read our contributing guide to get started.
License
This project is licensed under the MIT License - see the LICENSE file for details.
