Complete API documentation for React Agent UI.
Main component for rendering conversation events.
interface ConversationUIProps {
events: UIEvent[];
renderers?: Partial<RendererRegistry>;
onAction?: (actionId: string, eventId: string, data?: Record<string, unknown>) => void;
messageContainer?: React.ComponentType<MessageContainerProps>;
className?: string;
autoScroll?: boolean;
emptyComponent?: React.ComponentType;
debug?: boolean;
}Props:
- events (required): Array of UIEvent objects to render
- renderers: Override or add custom renderers (optional)
- onAction: Callback when action is triggered (optional)
- messageContainer: Custom message wrapper component (optional)
- className: Additional CSS class for root element (optional)
- autoScroll: Auto-scroll to latest message (default: true)
- emptyComponent: Component to show when no messages (optional)
- debug: Enable console logging (default: false)
Example:
import { ConversationUI } from "@react-agent-ui/core";
<ConversationUI
events={messages}
onAction={(actionId, eventId, data) => {
console.log("Action:", actionId);
}}
autoScroll
/>Wrapper component for individual messages.
interface MessageContainerProps {
role?: MessageRole;
children: React.ReactNode;
className?: string;
timestamp?: number;
}Props:
- role: Message role for styling ("user" | "assistant" | "system" | "tool")
- children: Message content
- className: Additional CSS class (optional)
- timestamp: Unix timestamp for display (optional)
Example:
const CustomContainer = ({ role, children, timestamp }) => (
<div className={`msg-${role}`}>
{children}
{timestamp && <span>{new Date(timestamp).toLocaleString()}</span>}
</div>
);
<ConversationUI
events={events}
messageContainer={CustomContainer}
/>Hook for managing conversation state and events.
function useConversationEvents(
options?: { debug?: boolean }
): UseConversationEventsReturnReturns:
interface UseConversationEventsReturn {
state: ConversationState;
addEvent: (event: UIEvent) => void;
updateEvent: (update: MessageUpdate) => void;
removeEvent: (eventId: string) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | undefined) => void;
clear: () => void;
}Methods:
- addEvent: Add a new event/message
- updateEvent: Update existing event (append/replace/partial)
- removeEvent: Remove event by ID
- setLoading: Set loading state
- setError: Set error message
- clear: Clear all messages
Example:
const { state, addEvent, updateEvent } = useConversationEvents();
// Add message
addEvent({
id: "msg-1",
role: "user",
kind: "text",
payload: { content: "Hello" }
});
// Stream update
updateEvent({
eventId: "msg-1",
operation: "append",
payload: " World"
});
// Use in render
<ConversationUI events={state.messages.map(m => m.event)} />Hook for handling action responses.
function useActionHandler(
options?: { onResponse?: (response: ActionResponse) => void; debug?: boolean }
): UseActionHandlerReturnReturns:
interface UseActionHandlerReturn {
handleAction: (actionId: string, eventId: string, context?: Record<string, unknown>) => void;
handleActionResponse: (response: ActionResponse) => void;
}Methods:
- handleAction: Process action trigger with metadata
- handleActionResponse: Send raw action response
Example:
const { handleAction } = useActionHandler({
onResponse: async (response) => {
await fetch("/api/actions", {
method: "POST",
body: JSON.stringify(response)
});
}
});
<ConversationUI
events={events}
onAction={(actionId, eventId, data) => {
handleAction(actionId, eventId, { custom: data });
}}
/>Base event interface.
interface UIEvent {
id: string;
role?: "system" | "user" | "assistant" | "tool";
kind: string;
payload: unknown;
meta?: Record<string, unknown>;
}Type-safe event with discriminated kind.
interface TypedUIEvent<K extends EventKind = EventKind, P = unknown>
extends Omit<UIEvent, "kind" | "payload"> {
kind: K;
payload: P;
}Example:
const textEvent: TypedUIEvent<"text", TextPayload> = {
id: "1",
kind: "text",
payload: { content: "Hello" }
};Streaming update operation.
interface MessageUpdate {
eventId: string;
operation: "append" | "replace" | "partial";
payload: unknown;
timestamp?: number;
}Operations:
- append: Add content (strings concat, arrays merge)
- replace: Replace entire payload
- partial: Merge object properties
Response sent when action is triggered.
interface ActionResponse {
actionId: string;
eventId: string;
context?: Record<string, unknown>;
timestamp: number;
}interface TextPayload {
content: string;
}interface MarkdownPayload {
content: string;
}interface ImagePayload {
src: string;
alt?: string;
width?: number;
height?: number;
caption?: string;
}interface TablePayload {
headers: string[];
rows: (string | number | boolean)[][];
title?: string;
sortable?: boolean;
}interface ChartPayload {
type: "line" | "bar" | "pie" | "scatter" | "area";
data: Record<string, unknown>[];
xAxis?: string;
yAxis?: string;
title?: string;
config?: Record<string, unknown>;
}interface ActionPayload {
id: string;
label: string;
variant?: "primary" | "secondary" | "danger" | "default";
description?: string;
data?: Record<string, unknown>;
}interface LoadingPayload {
message?: string;
}interface ErrorPayload {
message: string;
code?: string;
details?: unknown;
}TextRenderer- Plain textMarkdownRenderer- Markdown contentImageRenderer- Images with captionsTableRenderer- Tabular dataChartRenderer- Data chartsActionRenderer- Interactive buttonsLoadingRenderer- Loading stateErrorRenderer- Error messagesFallbackRenderer- Unknown event kinds
interface RendererProps<E extends UIEvent = UIEvent> {
event: E;
onAction?: (actionId: string, data?: Record<string, unknown>) => void;
className?: string;
style?: React.CSSProperties;
}const CustomRenderer: React.FC<RendererProps> = ({ event, onAction, className }) => {
return (
<div className={`custom-${event.kind} ${className}`}>
{/* render based on event.payload */}
</div>
);
};
<ConversationUI
events={events}
renderers={{ custom: CustomRenderer }}
/>Pure reducer for conversation state.
function conversationReducer(
state: ConversationState,
action: ConversationAction
): ConversationStateActions:
ADD_MESSAGE- Add new eventUPDATE_MESSAGE- Update with streaming operationREMOVE_MESSAGE- Remove by IDSET_LOADING- Set loading stateSET_ERROR- Set errorCLEAR_MESSAGES- Clear all
interface ConversationState {
messages: Message[];
loading: boolean;
error?: string;
}
interface Message {
event: UIEvent;
version: number;
timestamp: number;
isStreaming: boolean;
error?: string;
}.rai-message-container.rai-message-user.rai-message-assistant.rai-message-system.rai-message-tool.rai-message-content.rai-message-timestamp
.rai-text-renderer.rai-markdown-renderer.rai-image-renderer.rai-table-renderer.rai-chart-renderer.rai-action-renderer.rai-loading-renderer.rai-error-renderer.rai-fallback-renderer
.rai-action-button.rai-action-primary.rai-action-secondary.rai-action-danger.rai-action-default
See styles.css for complete styling reference.
Registry of all default renderers.
const DEFAULT_RENDERERS: RendererRegistry = {
text: TextRenderer,
markdown: MarkdownRenderer,
image: ImageRenderer,
chart: ChartRenderer,
table: TableRenderer,
action: ActionRenderer,
loading: LoadingRenderer,
error: ErrorRenderer,
}Renderer for unknown event kinds.
const FALLBACK_RENDERER = FallbackRenderer;UIEvent created
↓
Added to ConversationUI via events prop
↓
conversationReducer processes ADD_MESSAGE
↓
Message stored in state
↓
Renderer component selected and rendered
↓
Message displayed
MessageUpdate created
↓
updateEvent() called
↓
conversationReducer processes UPDATE_MESSAGE
↓
Reducer applies operation (append/replace/partial)
↓
Message version incremented
↓
React re-renders updated message
User clicks action button
↓
ActionRenderer.handleClick()
↓
onAction callback triggered
↓
handleAction() creates ActionResponse
↓
onResponse callback with ActionResponse
↓
Application sends to backend
const { addEvent, updateEvent } = useConversationEvents();
async function streamResponse() {
const msgId = `msg-${Date.now()}`;
addEvent({
id: msgId,
role: "assistant",
kind: "text",
payload: { content: "" }
});
const response = await fetch("/api/chat", { method: "POST" });
const reader = response.body?.getReader();
while (true) {
const { value, done } = await reader?.read() || {};
if (done) break;
const chunk = new TextDecoder().decode(value);
updateEvent({
eventId: msgId,
operation: "append",
payload: chunk
});
}
}const events: UIEvent[] = [
{ id: "q1", role: "assistant", kind: "text", payload: { content: "Name?" } },
{ id: "input-name", role: "assistant", kind: "action", payload: {
id: "submit-name",
label: "Submit",
variant: "primary"
}}
];interface CodePlaygroundPayload {
language: string;
code: string;
}
const event: TypedUIEvent<"code-playground", CodePlaygroundPayload> = {
id: "1",
kind: "code-playground",
payload: { language: "typescript", code: "console.log('hello')" }
};