The ChatWidget component
<ChatWidget> is the client component. It owns chat state, rendering, and calls
the route mounted by createChatHandler.
'use client';
import { ChatWidget } from '@mordn/chat-widget';
import '@mordn/chat-widget/styles.css';
export function Assistant() {
return <ChatWidget apiBase="/api/chat" />;
}That is the whole component. On mount the widget calls
GET ${apiBase}/bootstrap and the server returns the browser-safe half of the
canonical config — greeting, theme, display, features, starter prompts.
Warning
The widget sends no userId, agentId, model, or system prompt, and there is
no X-User-Id header in the client transport. Identity is resolved on your
server by getUserId (the getChatUserId stub the CLI scaffolds), so the
browser cannot assert who it is or which history it may read. Appearance and
model policy are server-owned too — see
createChatHandler.
Transport
apiBasestringdefault: /api/chatBase route for bootstrap and the chat, history, upload, memory, and feedback
subpaths.
headersRecord<string, string>Generic transport headers, for example a host's CSRF token. These are browser controlled; never treat them as identity on the server.
requestCredentialsRequestCredentialsFetch credential mode. Use include only when a cross-origin handler authenticates
with cookies, paired with an explicit server cors policy.
conversationIdstringOpen or create a specific conversation id.
initialMessagesUIMessage[]Initial client message state.
classNamestringClass name applied to the root container.
Configuration
Appearance and client behaviour are not props. They live in the client
half of the canonical config document, which normally arrives from the server on
bootstrap. To set it in code instead — for local development, or when you are
not publishing from the dashboard — pass the same canonical shape as config.
configAgentConfigFull canonical config for caller overrides or an authenticated preview. Explicit
values are merged over the published config returned by bootstrap.
<ChatWidget
apiBase="/api/chat"
config={{
schemaVersion: 1,
runtime: { model: 'anthropic/claude-sonnet-4-5' },
client: {
greeting: 'How can I help?',
theme: {
backgroundColor: '#171717',
textColor: '#ededed',
primaryColor: '#3b82f6',
},
display: { layout: 'popup', size: 'default' },
features: { fileUpload: true },
starterPrompts: [{ title: 'What can you help me with?' }],
},
}}
/>The fields below are all members of config.client.
config.client.greetingstringEmpty-state greeting headline.
config.client.subGreetingstringFaint line under the greeting headline.
config.client.assistantNamestringThe assistant's name, used as its identity in the UI.
config.client.capabilitiesPromptstringFallback prompt affordance shown in the empty state.
config.client.starterPrompts{ title: string; subtitle?: string }[]Static empty-state prompts.
config.client.feedbackbooleanShow answer ratings.
config.client.streamingThrottleMsnumberThrottle interval for streaming re-renders.
config.client.persistStatebooleandefault: truePersist the uncontrolled popup's open/closed state, scoped to the opaque
storage scope issued by bootstrap.
config.client.allowAutoReopenbooleandefault: falseAllow host-driven programmatic reopen after dismissal. Closing is always allowed.
Theme
ThemeConfig contains exactly three required hex colors. Omit theme for the
stock palette. Partial themes, mode, and arbitrary tokens are not public
APIs.
interface ThemeConfig {
backgroundColor: string;
textColor: string;
primaryColor: string;
}See Theming.
Features
config.client.features.fileUploadbooleanShow upload controls. The server also needs a StorageAdapter.
config.client.features.fileUploadAcceptstringdefault: image/*Browser file-picker filter; mirror the server allow-list.
config.client.features.fileUploadMaxBytesnumberClient UX cap only. UploadPolicy.maxBytes is authoritative.
config.client.features.webSearchbooleanExpose the web-search feature state in the UI.
Display
config.client.display.layout'popup' | 'inline' | 'page'default: popupFloating panel, embedded surface, or full-height chat page.
config.client.display.size'compact' | 'default' | 'large' | 'full'default: defaultPopup width preset.
config.client.display.widthstringconfig.client.display.resizablebooleandefault: trueconfig.client.display.defaultOpenbooleandefault: falseconfig.client.display.starterPromptsLayout'list' | 'grid'default: listconfig.client.display.showToggleButtonbooleandefault: trueconfig.client.display.toggleButtonPosition{ bottom?: string; right?: string }Custom launcher position. Defaults include safe-area insets.
config.client.display.keyboardShortcutstring | falseOpen the widget with a keyboard combo such as "mod+i". Off by default; the
widget never claims a host keybinding implicitly.
Panel state
openbooleanonOpenChange(open: boolean) => voidonStateChange(open: boolean) => voidonClose() => voidheaderActionsReactNodePanel persistence and programmatic reopen are configured with
config.client.persistState and config.client.allowAutoReopen above.
Guidance and extension points
getStarterPrompts() => StarterPrompt[] | Promise<StarterPrompt[]>Runtime prompts; a non-empty result wins over the static
config.client.starterPrompts list.
contextChatContext | 'auto' | (() => ChatContext | Promise<ChatContext>)Per-turn client context: a static object, 'auto' for the built-in safe page
capture, or a function resolved fresh at send time. Ignored by default unless
the server validates it with getContext or opts in with trustClientContext.
inputPluginsInputPlugin[]toolRenderersRecord<string, ToolRenderer>actionRenderersRecord<string, ActionRenderer>onFeedback(feedback: FeedbackEvent) => voidObserve client submissions. Ratings are shown only when
config.client.feedback is on.
Imperative handle
interface ChatWidgetHandle {
open(): void;
close(): void;
toggle(): void;
readonly isOpen: boolean;
}Opening obeys config.client.allowAutoReopen; closing is always allowed. See
AI UX extras for examples.