| title | Plugin Chatbot |
|---|
import { InteractiveDemo } from '@/app/components/InteractiveDemo'; import { PluginLoader } from '@/app/components/PluginLoader';
Chat interface component with message history, typing indicators, and customizable avatars.
v5.4+ — the chat surface is now composed from vendored Vercel AI Elements (MIT). The public
chatbotschema andChatbotEnhanced/FloatingChatbotprops are unchanged. A new optionalsuggestions: string[]field renders chip prompts in the empty state.
For advanced compositions, the underlying elements are re-exported:
import { AIElements } from '@object-ui/plugin-chatbot';
// AIElements.Conversation, AIElements.Message, AIElements.PromptInput, ...npm install @object-ui/plugin-chatbot<PluginLoader plugins={['chatbot']}>
// Import once in your app entry point
import '@object-ui/plugin-chatbot'
import type { ChatbotSchema } from '@object-ui/types'
// Use in schemas
const schema: ChatbotSchema = {
type: 'chatbot',
messages: [
{
id: '1',
role: 'assistant',
content: 'Hello! How can I help you?'
}
],
placeholder: 'Type your message...',
autoResponse: true
}- Message History: Display chat messages with user and assistant roles
- System Messages: Show system notifications in the chat
- Timestamps: Optional timestamp display for each message
- Custom Avatars: Configurable avatar images and fallback text
- Auto-scroll: Automatically scroll to newest messages
- Typing Indicator: Built-in typing indicator component
- Auto-response: Demo mode with automatic responses
- Responsive Floating Panel: Console assistants keep the panel inside safe browser gutters and hide the FAB while the chat is open
- Conversation States: Empty streaming messages render as an assistant responding indicator, the submit control becomes Stop while streaming, and backend errors collapse into a retryable notice with optional details
- Lightweight: Pure React components with minimal dependencies
{
type: 'chatbot',
messages?: ChatMessage[],
placeholder?: string,
showTimestamp?: boolean,
disabled?: boolean,
userAvatarUrl?: string,
userAvatarFallback?: string,
assistantAvatarUrl?: string,
assistantAvatarFallback?: string,
maxHeight?: string,
autoResponse?: boolean,
autoResponseText?: string,
autoResponseDelay?: number,
onSend?: (content: string, messages: ObjectChatMessage[]) => void,
className?: string,
// AI / service-ai integration fields
api?: string,
conversationId?: string,
systemPrompt?: string,
model?: string,
streamingEnabled?: boolean,
headers?: Record<string, string>,
requestBody?: Record<string, unknown>,
maxToolRoundtrips?: number, // deprecated - inert, see below
onError?: (error: Error) => void,
}
{
id: string,
role: 'user' | 'assistant' | 'system' | 'tool',
content: string,
timestamp?: string | Date,
metadata?: any,
streaming?: boolean,
toolInvocations?: ChatToolInvocation[],
}
showTimestamp through onSend below are declared on ChatbotSchema
(@object-ui/types) — previously they existed only in an anonymous type local
to the renderer, referenceable, validatable and documentable by nothing
outside that one file (objectui#6169).
This page documents three registrations - chatbot, chatbot-enhanced and
chatbot-floating - and they do not all read the same keys. A row whose
description carries no bolded scope note is read by all three. The rows only
some of them read say so in bold at the start of the description, and name what
to author instead on the registrations that ignore the key.
Each registration has its own importable authoring-face type in
@object-ui/types (objectui#7655): ChatbotSchema for chatbot,
ChatbotEnhancedSchema for chatbot-enhanced and ChatbotFloatingSchema for
chatbot-floating. Each declares exactly the keys its registration reads - the
twenty shared rows below are one declaration the two newer faces pick off
ChatbotSchema by name, and a scoped row is declared only on the face(s) whose
registration reads it. A key a registration ignores is therefore not a declared
member of its type. It still type-checks (BaseSchema ends in an index
signature, so an unlisted key is any rather than an error) and still parses
(the Zod twins are .passthrough()). On chatbot and chatbot-enhanced it is
then dropped silently at render time. chatbot-floating is different today: its
registration forwards the whole authored node to the panel through an
unfiltered props spread, so some keys its type does not declare
(processVisibility, surface, showAvatars) do reach the panel - an
accidental channel, measured and tracked as objectui#7708, not a contract to
author against. That is why the scope is spelled out here as well as in the
types.
The table below is that shared chat surface. chatbot-floating declares
six more keys of its own - the six floatingConfig entries - which no row
below carries and which the other two registrations have no trigger or panel
to apply. They are documented in their own table after this one, under
chatbot-floating panel and trigger keys.
| Property | Type | Default | Description |
|---|---|---|---|
messages |
array | [] |
Initial chat messages |
placeholder |
string | 'Type your message...' |
Input field placeholder text |
showTimestamp |
boolean | false |
Display timestamps for messages |
disabled |
boolean | false |
Disable chat input |
userAvatarUrl |
string | - | URL for user avatar image |
userAvatarFallback |
string | 'You' |
Fallback text for user avatar |
assistantAvatarUrl |
string | - | URL for assistant avatar image |
assistantAvatarFallback |
string | 'AI' |
Fallback text for assistant avatar |
maxHeight |
string | '500px' |
chatbot and chatbot-enhanced only (declared on ChatbotSchema and ChatbotEnhancedSchema; ChatbotFloatingSchema does not declare it). Maximum height of the chat message container, as a CSS length. chatbot-floating does not read it: its panel is sized by floatingConfig.panelHeight (a number of pixels, default 520), and the panel pins its inner chat to maxHeight: '100%' so it fills that panel - a maxHeight authored on a floating node would be overridden even if it were forwarded. Size a floating chatbot with floatingConfig.panelHeight instead |
autoResponse |
boolean | false |
Enable auto-response (demo mode, ignored when api is set) |
autoResponseText |
string | - | Text for auto-response |
autoResponseDelay |
number | 1000 |
Delay before auto-response (ms) |
onSend |
function | - | Callback when message is sent |
className |
string | '' |
Additional Tailwind CSS classes |
api |
string | - | Backend SSE endpoint (e.g., /api/v1/ai/chat). Enables AI streaming mode |
conversationId |
string | - | Multi-turn conversation identifier |
systemPrompt |
string | - | System prompt to configure assistant behavior |
model |
string | - | AI model identifier (e.g., gpt-4o) |
streamingEnabled |
boolean | true |
Enable SSE streaming for AI responses |
headers |
object | - | Additional headers for API requests |
requestBody |
object | - | Additional body parameters sent with each API request. Authored on the node as requestBody; the renderer forwards it to the chat runtime under its own body option. Writing body on the node instead sets the base schema's children container and never reaches the API |
maxToolRoundtrips |
number | - | Deprecated - has no effect. Nothing reads this value, so it never capped anything. Cap tool-calling loops on the agent instead (planning.maxIterations). Still accepted so existing documents keep parsing; slated for removal in a future major |
surface |
'card' | 'plain' |
'card' |
chatbot-enhanced only (declared on ChatbotEnhancedSchema). Controls whether the chat renders as a bordered panel ('card') or a frameless full-page workspace ('plain'). chatbot renders the plain chat component, which has no such chrome to switch, and does not read this key. chatbot-floating has no named read for it and ChatbotFloatingSchema does not declare it; its panel is a ChatbotEnhanced, and an authored value currently reaches that panel only through the registration's unfiltered props spread (objectui#7708) - not a contract to author against |
processVisibility |
'hidden' | 'summary' | 'debug' |
'summary' |
chatbot-enhanced only (declared on ChatbotEnhancedSchema; ChatbotSchema still declares it too, though the chatbot registration has no read for it). Controls how much agent reasoning and tool detail is shown. chatbot renders the plain chat component, which has no agent-process display to configure at all - switch the node to chatbot-enhanced if you need one. chatbot-floating has no named read for it and ChatbotFloatingSchema does not declare it; an authored value currently reaches its panel only through the registration's unfiltered props spread (objectui#7708), which is not a contract - there is no floating-side substitute to author |
onError |
function | - | Error callback for streaming/API errors |
The six keys below are declared in the chatbot-floating registration's own
inputs (packages/plugin-chatbot/src/renderer.tsx). They configure the
floating action button and the panel it opens; the chatbot and
chatbot-enhanced registrations render neither and ignore them. floatingConfig
is declared on ChatbotSchema and on ChatbotFloatingSchema alike
(objectui#7655 declared the floating face with the same member; ChatbotSchema
kept its own), so authoring it on an inline node type-checks and parses - and
is dropped at render time, because the chatbot node never read it.
There is no displayMode key. The presentation is selected by the node's
own type: author a chatbot-floating node for the trigger-and-panel
presentation, and a chatbot or chatbot-enhanced node for an inline one.
displayMode ('inline' | 'floating') used to be declared on both faces,
offered as a Display Mode control in the designer and seeded as
'floating' into every node the designer created - and read by nothing: it was
a second spelling of the choice type already makes, so 'inline' on a
chatbot-floating node changed nothing and 'floating' on a chatbot node
produced no trigger. objectui#7654 retired it (maintainer ruling, 2026-09-05):
the declaration is a never tombstone on ChatbotSchema and
ChatbotFloatingSchema, so writing the key against either face is now a
compile error, and the designer control and default are gone. Stored documents
that still carry the key parse exactly as they did - it never had a Zod arm and
the twins are passthrough - and the value is ignored at render time, as it
always was.
| Property | Type | Default | Description |
|---|---|---|---|
floatingConfig.position |
'bottom-right' | 'bottom-left' |
'bottom-right' |
Corner the trigger sits in; the panel is anchored to the same side |
floatingConfig.defaultOpen |
boolean | false |
Whether the panel is already open when the node mounts |
floatingConfig.panelWidth |
number | 400 |
Panel width in pixels, applied from the sm breakpoint up - below it the panel is full-bleed. Snapped to a step, see below |
floatingConfig.panelHeight |
number | 520 |
Panel height in pixels, as a number - not a CSS length string. This is the key that sizes a floating chatbot: maxHeight is a string, belongs to the two inline registrations, and is not read here. Snapped to a step, see below |
floatingConfig.title |
string | 'Chat' |
Text in the panel header, and the panel's aria-label |
floatingConfig.triggerSize |
number | 56 |
Diameter of the floating action button in pixels. Snapped to a step, see below |
The three size keys snap to the nearest declared step. FloatingChatbotPanel
and FloatingChatbotTrigger resolve each number through a fixed table of
Tailwind classes and fall back to the closest entry, so a number outside the
table does not render at that size - a panelHeight of 530 renders at 520:
panelWidth- 300, 320, 340, 360, 380, 400, 420, 440, 450, 460, 480, 500, 520, 560, 600, 640, 720, 800panelHeight- 360, 400, 420, 440, 480, 500, 520, 560, 600, 640, 720, 800triggerSize- 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80
On small screens panelHeight is additionally capped to the viewport
(min(step, 100svh - 6rem - safe-area-inset-bottom)), and while the panel is
fullscreen it ignores both size keys and fills the screen.
Authored on the node, with the node's own type - ChatbotFloatingSchema pins
type to 'chatbot-floating' and declares floatingConfig, so this fence
compiles against the published types. (Until objectui#7655 no type could
annotate a floating node - ChatbotSchema pins type to 'chatbot' - and
this example had to be untyped JSON.)
import type { ChatbotFloatingSchema } from '@object-ui/types';
const supportChat: ChatbotFloatingSchema = {
type: 'chatbot-floating',
messages: [], // seed with your own ChatMessage values
floatingConfig: {
position: 'bottom-left',
defaultOpen: false,
panelWidth: 400,
panelHeight: 520,
title: 'Support',
triggerSize: 56,
},
placeholder: 'Ask us anything...',
};The config object also has its own exported type:
import type { FloatingChatbotConfig } from '@object-ui/types';
const floatingConfig: FloatingChatbotConfig = {
position: 'bottom-left',
panelWidth: 400,
panelHeight: 520, // pixels, as a number - '520px' does not type-check
title: 'Support',
triggerSize: 56,
};The chatbot supports two modes, automatically selected based on the api field:
When api is not set, the chatbot operates in local mode with optional auto-response:
import type { ChatbotSchema } from '@object-ui/types';
const schema: ChatbotSchema = {
type: 'chatbot',
messages: [], // seed with your own ChatMessage values
autoResponse: true,
autoResponseText: 'Thanks!',
autoResponseDelay: 1000,
};When api is set, the chatbot uses @ai-sdk/react for real SSE streaming:
import type { ChatbotSchema } from '@object-ui/types';
const schema: ChatbotSchema = {
type: 'chatbot',
api: '/api/v1/ai/chat',
model: 'gpt-4o',
systemPrompt: 'You are a helpful assistant.',
streamingEnabled: true,
messages: [],
};Messages from the user appear on the right side with primary styling:
import type { ChatMessage } from '@object-ui/types';
const userMessage: ChatMessage = {
id: '1',
role: 'user',
content: 'Hello!',
timestamp: '10:30 AM',
};Messages from the assistant appear on the left side:
import type { ChatMessage } from '@object-ui/types';
const assistantMessage: ChatMessage = {
id: '2',
role: 'assistant',
content: 'Hi! How can I help?',
timestamp: '10:30 AM',
};System messages appear centered with muted styling:
import type { ChatMessage } from '@object-ui/types';
const systemMessage: ChatMessage = {
id: '3',
role: 'system',
content: 'Chat session started',
};Tool messages represent results from tool invocations during AI streaming. They are generated automatically by the vercel/ai SDK when the backend performs tool calls (e.g., fetching weather, querying a database). The SDK may emit role: 'tool' messages as well as populate the assistant message's toolInvocations array.
By default, ChatbotEnhanced renders tool invocations as a compact agent activity summary. Repeated calls are grouped, raw tool names are hidden, and reasoning text is not shown. Use processVisibility="debug" for developer/admin views that need the full reasoning panel, raw tool names, parameters, and results. Use processVisibility="hidden" to suppress non-interactive activity entirely; approval and draft-review actions remain visible.
Use surface="plain" for full-page chat workspaces where the surrounding app
already provides navigation chrome. The default surface="card" remains a
better fit for embedded dashboards, side panels, and floating chat windows.
Both keys are authorable as metadata on a chatbot-enhanced node, typed with
that node's own face (objectui#7655):
import type { ChatbotEnhancedSchema } from '@object-ui/types';
const workspace: ChatbotEnhancedSchema = {
type: 'chatbot-enhanced',
messages: [], // seed with your own ChatMessage values
api: '/api/v1/ai/chat',
surface: 'plain',
processVisibility: 'debug',
enableFileUpload: true,
};Console chat surfaces also keep a sanitized browser-side display cache for the current conversation. When a conversation can be reopened but the server returns no message rows, the UI restores user/assistant text and grouped tool names plus states. Reasoning, tool parameters, and raw tool results are not stored in this cache.
import type { ChatMessage } from '@object-ui/types';
const toolMessage: ChatMessage = {
id: '4',
role: 'assistant',
content: 'The weather in SF is 68°F.',
toolInvocations: [
{
toolCallId: 'tc-1',
toolName: 'getWeather',
args: { city: 'San Francisco' },
result: { temp: 68, condition: 'Sunny' },
state: 'result',
},
],
};import type { ChatbotSchema } from '@object-ui/types';
const aiChat: ChatbotSchema = {
type: 'chatbot',
messages: [
{
id: 'welcome',
role: 'assistant',
content: 'Hello! I\'m your AI assistant. Ask me anything!'
}
],
placeholder: 'Ask me a question...',
assistantAvatarFallback: 'AI',
userAvatarFallback: 'You',
autoResponse: true,
autoResponseText: 'That\'s a great question! Let me think about that...',
maxHeight: '600px'
}import type { ChatbotSchema } from '@object-ui/types';
const supportChat: ChatbotSchema = {
type: 'chatbot',
messages: [
{
id: 'sys-1',
role: 'system',
content: 'Ticket #12345 - Account Access Issue'
},
{
id: '1',
role: 'assistant',
content: 'Hi! I\'m here to help with your account access issue.',
avatarFallback: 'SP',
timestamp: '2:15 PM'
}
],
placeholder: 'Describe your issue...',
showTimestamp: true,
userAvatarFallback: 'JD',
assistantAvatarFallback: 'SP',
className: 'w-full max-w-3xl mx-auto'
};import type { ChatbotSchema } from '@object-ui/types';
const salesBot: ChatbotSchema = {
type: 'chatbot',
messages: [
{
id: '1',
role: 'assistant',
content: 'Welcome! I\'m here to help you find the perfect product. What are you looking for today?',
avatarFallback: 'SB'
}
],
placeholder: 'Tell us what you need...',
assistantAvatarFallback: 'SB',
userAvatarFallback: 'You',
autoResponse: true,
autoResponseText: 'Great choice! Let me show you some options...',
autoResponseDelay: 1200,
maxHeight: '500px',
className: 'border-2 border-primary rounded-xl'
};import type { ChatbotSchema } from '@object-ui/types';
const multiAgentChat: ChatbotSchema = {
type: 'chatbot',
messages: [
{
id: '1',
role: 'assistant',
content: 'Hello! Sarah from Sales here.',
avatarFallback: 'SA'
},
{
id: '2',
role: 'user',
content: 'I need help with pricing',
avatarFallback: 'CU'
},
{
id: '3',
role: 'system',
content: 'Transferring to Finance team...'
},
{
id: '4',
role: 'assistant',
content: 'Hi! Mike from Finance. I can help with that.',
avatarFallback: 'MI'
}
],
showTimestamp: true,
userAvatarFallback: 'CU'
};import type { ChatbotSchema } from '@object-ui/types';
const schema: ChatbotSchema = {
type: 'chatbot',
userAvatarUrl: 'https://example.com/user-avatar.jpg',
assistantAvatarUrl: 'https://example.com/bot-avatar.jpg',
messages: [], // seed with your own ChatMessage values
};When images aren't available, fallback text is displayed:
import type { ChatbotSchema } from '@object-ui/types';
const schema: ChatbotSchema = {
type: 'chatbot',
userAvatarFallback: 'JD', // User initials
assistantAvatarFallback: 'AI', // Bot identifier
messages: [], // seed with your own ChatMessage values
};Override avatars for individual messages:
import type { ChatMessage } from '@object-ui/plugin-chatbot';
const message: ChatMessage = {
id: '1',
role: 'assistant',
content: 'Message content',
avatar: 'https://example.com/special-avatar.jpg',
avatarFallback: 'SP',
};Handle message sending in your application:
import type { ChatbotSchema } from '@object-ui/types';
const schema: ChatbotSchema = {
type: 'chatbot',
messages: [], // seed with your own ChatMessage values
onSend: (content, allMessages) => {
console.log('User sent:', content);
console.log('All messages:', allMessages);
// Send to your backend
fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message: content }),
});
},
};import type { ChatbotSchema } from '@object-ui/types';
const schema: ChatbotSchema = {
type: 'chatbot',
className: 'w-full max-w-2xl mx-auto border-2 rounded-xl shadow-lg',
maxHeight: '600px',
messages: [], // seed with your own ChatMessage values
};import type { ChatbotSchema } from '@object-ui/types';
const schema: ChatbotSchema = {
type: 'chatbot',
maxHeight: '400px', // or use Tailwind: 'h-96'
className: 'sm:max-h-[500px] lg:max-h-[600px]',
messages: [], // seed with your own ChatMessage values
};Each of the three registrations has its own authoring-face type:
ChatbotSchema (chatbot), ChatbotEnhancedSchema (chatbot-enhanced) and
ChatbotFloatingSchema (chatbot-floating) - see the typed examples under
Tool Messages and chatbot-floating panel and trigger keys above.
import type { ChatbotSchema, ChatbotEnhancedSchema, ChatbotFloatingSchema, ChatMessage, ChatToolInvocation } from '@object-ui/types'
import { useObjectChat } from '@object-ui/plugin-chatbot'
// Basic messages
const messages: ChatMessage[] = [
{
id: '1',
role: 'assistant',
content: 'Hello!'
}
]
// Local/demo mode
const demoSchema: ChatbotSchema = {
type: 'chatbot',
messages,
placeholder: 'Type here...',
showTimestamp: true
}
// AI streaming mode (service-ai)
const aiSchema: ChatbotSchema = {
type: 'chatbot',
messages: [],
api: '/api/v1/ai/chat',
model: 'gpt-4o',
systemPrompt: 'You are a helpful assistant.',
streamingEnabled: true,
conversationId: 'conv-123',
}
You AUTHOR with @object-ui/types' ChatMessage. What useObjectChat HANDS
BACK — from messages and from onSend(content, messages) — is
ObjectChatMessage, exported from @object-ui/plugin-chatbot:
import type { ObjectChatMessage } from '@object-ui/plugin-chatbot'
It is the authoring shape plus the render-only keys API mode really carries
(buildProgress, blueprintProgress, charts, and pendingActionId /
draftReview / proposedPlan / proposedChanges / builderHandoff on each
tool invocation — the approval card, the "Review N changes" affordance, the plan
card, the build panel, the inline charts), with timestamp narrowed to string
because both modes absorb an authored Date before emitting.
It is a subtype of the authoring ChatMessage, so an onSend callback that
already declares ChatMessage[] keeps type-checking; naming ObjectChatMessage
is what lets it read those keys. Rebuilding a message field-by-field from the
authoring type drops every one of them — silently, and with the compiler's
agreement (objectui#4424).